Course 5: Forex & Futures Algorithmic Trading
Course Overview
| Duration | Modules | Exercises | Level |
|---|---|---|---|
| ~40 hours | 14 + Capstone | ~90 | Intermediate |
Course Description
Trade the world's largest markets. Master forex and futures, understand leverage, implement global macro strategies, and build systems for 24-hour operation.
Prerequisites
- Course 0: Python for Finance - Python programming, pandas, data analysis
What You'll Learn
- Navigate the forex market (currency pairs, pips, lots, trading sessions)
- Understand futures contracts (expiration, rollover, basis, contango/backwardation)
- Master leverage and margin management
- Connect to OANDA API for data and trading
- Implement forex-specific strategies (trend following, carry trade, news trading)
- Build commodity trading systems (gold, oil, agricultural)
- Manage risk with proper position sizing and gap risk handling
- Deploy 24-hour automated trading systems
Course Structure
Part 1: Market Fundamentals
Understand the unique characteristics of forex and futures markets
| Module | Title | Key Topics |
|---|---|---|
| 1 | Forex Market Basics | Currency pairs, market structure, sessions, pips & lots |
| 2 | Futures Market Basics | Contract specs, expiration, basis, rollover |
| 3 | Leverage & Margin | Leverage mechanics, margin requirements, risk management |
| 4 | Data & Tools | OANDA API, data sources, real-time feeds |
Part 2: Analysis & Strategies
Learn analysis techniques and trading strategies specific to forex/futures
| Module | Title | Key Topics |
|---|---|---|
| 5 | Technical Analysis | Chart patterns, forex indicators, multi-timeframe |
| 6 | Fundamental Analysis | Economic indicators, central banks, calendar |
| 7 | Commodity Trading | Gold, oil, agricultural, commodity currencies |
| 8 | Trading Strategies | Trend following, range, carry trade, news trading |
Part 3: Risk & Execution
Manage risk and execute trades in leveraged markets
| Module | Title | Key Topics |
|---|---|---|
| 9 | Risk Management | Forex/futures risk, portfolio risk, gap risk |
| 10 | Backtesting | Spread costs, leveraged backtesting, tick data |
| 11 | Live Trading | Brokers, OANDA execution, MetaTrader, 24h operation |
Part 4: Advanced Topics
Master advanced strategies and build production systems
| Module | Title | Key Topics |
|---|---|---|
| 12 | Advanced Strategies | Currency indices, spreads, options, seasonality |
| 13 | Automation & Monitoring | 24/7 scheduling, alerts, performance tracking |
| 14 | Trading Psychology & Journaling | Psychology, journals, performance review |
Capstone Project
Build a 24-Hour Forex/Futures Trading System with multi-currency monitoring, economic calendar integration, leveraged risk management, and automated journaling.
Why Forex & Futures?
The World's Largest Markets
- Forex: $7.5 trillion daily volume (largest financial market)
- Futures: Standardized contracts for commodities, indices, currencies
- 24-hour trading: Opportunities around the clock
- High liquidity: Tight spreads on major pairs
Unique Characteristics
| Feature | Forex | Futures | Stocks |
|---|---|---|---|
| Trading Hours | 24/5 | Near 24/5 | Market hours only |
| Leverage | 50:1 to 500:1 | 10:1 to 20:1 | 2:1 to 4:1 |
| Minimum Capital | Very low | Moderate | Moderate |
| Settlement | T+2 or rolling | Physical/Cash | T+2 |
| Short Selling | Native (sell currency) | Easy | Requires borrowing |
Key Concepts to Master
- Leverage - Amplifies both gains and losses
- Pips & Lots - Forex-specific terminology
- Rollover/Swap - Interest on held positions
- Basis & Contango - Futures pricing dynamics
- Economic Calendar - News drives forex markets
Environment Setup
# Install required packages (uncomment to run)
# !pip install oandapyV20 MetaTrader5 ta yfinance pandas numpy matplotlib
# Core imports for the course
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from datetime import datetime, timedelta
# Set display options
pd.set_option('display.max_columns', 20)
pd.set_option('display.width', 200)
plt.style.use('seaborn-v0_8-whitegrid')
print("Environment ready!")
print(f"pandas version: {pd.__version__}")
print(f"numpy version: {np.__version__}")
Forex Market Preview
# Major currency pairs
major_pairs = {
'EUR/USD': 'Euro vs US Dollar',
'GBP/USD': 'British Pound vs US Dollar',
'USD/JPY': 'US Dollar vs Japanese Yen',
'USD/CHF': 'US Dollar vs Swiss Franc',
'AUD/USD': 'Australian Dollar vs US Dollar',
'USD/CAD': 'US Dollar vs Canadian Dollar',
'NZD/USD': 'New Zealand Dollar vs US Dollar'
}
print("Major Currency Pairs")
print("=" * 50)
for pair, description in major_pairs.items():
print(f"{pair:12} - {description}")
# Trading sessions (UTC times)
sessions = {
'Sydney': ('21:00', '06:00', 'AUD, NZD'),
'Tokyo': ('00:00', '09:00', 'JPY pairs'),
'London': ('08:00', '17:00', 'EUR, GBP'),
'New York': ('13:00', '22:00', 'USD pairs')
}
print("\nForex Trading Sessions (UTC)")
print("=" * 50)
print(f"{'Session':12} {'Open':8} {'Close':8} {'Active Pairs'}")
print("-" * 50)
for session, (open_time, close_time, pairs) in sessions.items():
print(f"{session:12} {open_time:8} {close_time:8} {pairs}")
# Pip value calculation example
def calculate_pip_value(pair: str, lot_size: float = 1.0, account_currency: str = 'USD') -> float:
"""
Calculate pip value for a forex pair.
Args:
pair: Currency pair (e.g., 'EUR/USD')
lot_size: Position size in lots (1 lot = 100,000 units)
account_currency: Account denomination
Returns:
Pip value in account currency
"""
# Standard lot = 100,000 units
units = lot_size * 100_000
# For pairs ending in USD, 1 pip = $10 per standard lot
if pair.endswith('/USD'):
pip_value = units * 0.0001
# For JPY pairs, pip is 0.01
elif 'JPY' in pair:
pip_value = units * 0.01 / 150 # Approximate USD/JPY rate
else:
pip_value = units * 0.0001
return pip_value
# Example calculations
print("\nPip Values (1 Standard Lot = 100,000 units)")
print("=" * 50)
for pair in ['EUR/USD', 'GBP/USD', 'USD/JPY']:
pip_val = calculate_pip_value(pair, lot_size=1.0)
print(f"{pair}: ${pip_val:.2f} per pip")
Futures Market Preview
# Popular futures contracts
futures_contracts = {
'ES': {'name': 'E-mini S&P 500', 'exchange': 'CME', 'tick_size': 0.25, 'tick_value': 12.50},
'NQ': {'name': 'E-mini Nasdaq 100', 'exchange': 'CME', 'tick_size': 0.25, 'tick_value': 5.00},
'CL': {'name': 'Crude Oil', 'exchange': 'NYMEX', 'tick_size': 0.01, 'tick_value': 10.00},
'GC': {'name': 'Gold', 'exchange': 'COMEX', 'tick_size': 0.10, 'tick_value': 10.00},
'6E': {'name': 'Euro FX', 'exchange': 'CME', 'tick_size': 0.00005, 'tick_value': 6.25},
'ZB': {'name': '30-Year Treasury Bond', 'exchange': 'CBOT', 'tick_size': 1/32, 'tick_value': 31.25}
}
print("Popular Futures Contracts")
print("=" * 70)
print(f"{'Symbol':8} {'Name':25} {'Exchange':10} {'Tick Size':12} {'Tick Value'}")
print("-" * 70)
for symbol, info in futures_contracts.items():
tick_str = f"{info['tick_size']:.5f}".rstrip('0').rstrip('.')
print(f"{symbol:8} {info['name']:25} {info['exchange']:10} {tick_str:12} ${info['tick_value']:.2f}")
# Contract month codes
month_codes = {
'F': 'January', 'G': 'February', 'H': 'March', 'J': 'April',
'K': 'May', 'M': 'June', 'N': 'July', 'Q': 'August',
'U': 'September', 'V': 'October', 'X': 'November', 'Z': 'December'
}
print("\nFutures Contract Month Codes")
print("=" * 30)
for code, month in month_codes.items():
print(f"{code} = {month}")
# Basis calculation example
def calculate_basis(spot_price: float, futures_price: float) -> dict:
"""
Calculate futures basis and market condition.
Args:
spot_price: Current spot price
futures_price: Futures contract price
Returns:
Dictionary with basis and market condition
"""
basis = futures_price - spot_price
basis_pct = (basis / spot_price) * 100
if basis > 0:
condition = 'Contango'
elif basis < 0:
condition = 'Backwardation'
else:
condition = 'Flat'
return {
'basis': basis,
'basis_pct': basis_pct,
'condition': condition
}
# Example: Gold futures
spot_gold = 2050.00
futures_gold = 2065.50
result = calculate_basis(spot_gold, futures_gold)
print("\nGold Futures Basis Analysis")
print("=" * 40)
print(f"Spot Price: ${spot_gold:.2f}")
print(f"Futures Price: ${futures_gold:.2f}")
print(f"Basis: ${result['basis']:.2f} ({result['basis_pct']:.2f}%)")
print(f"Market: {result['condition']}")
Leverage Preview
# Leverage impact visualization
def simulate_leveraged_returns(
price_change_pct: float,
leverage_levels: list = [1, 10, 50, 100]
) -> pd.DataFrame:
"""
Simulate returns at different leverage levels.
"""
results = []
for leverage in leverage_levels:
leveraged_return = price_change_pct * leverage
results.append({
'Leverage': f'{leverage}:1',
'Price Change': f'{price_change_pct:.1f}%',
'Account Return': f'{leveraged_return:.1f}%'
})
return pd.DataFrame(results)
# Show impact of 1% price move
print("Impact of 1% Price Move at Different Leverage Levels")
print("=" * 55)
df = simulate_leveraged_returns(1.0)
print(df.to_string(index=False))
print("\nImpact of -2% Price Move at Different Leverage Levels")
print("=" * 55)
df = simulate_leveraged_returns(-2.0)
print(df.to_string(index=False))
# Visualize leverage impact
fig, axes = plt.subplots(1, 2, figsize=(12, 5))
# Price changes from -5% to +5%
price_changes = np.linspace(-5, 5, 100)
leverage_levels = [1, 10, 50, 100]
# Plot returns
colors = ['green', 'blue', 'orange', 'red']
for leverage, color in zip(leverage_levels, colors):
returns = price_changes * leverage
axes[0].plot(price_changes, returns, label=f'{leverage}:1', color=color, linewidth=2)
axes[0].axhline(y=0, color='black', linestyle='-', linewidth=0.5)
axes[0].axvline(x=0, color='black', linestyle='-', linewidth=0.5)
axes[0].axhline(y=-100, color='red', linestyle='--', linewidth=1, alpha=0.7)
axes[0].set_xlabel('Price Change (%)')
axes[0].set_ylabel('Account Return (%)')
axes[0].set_title('Leverage Impact on Returns')
axes[0].legend()
axes[0].set_ylim(-150, 150)
axes[0].grid(True, alpha=0.3)
# Plot margin call threshold
leverage_range = np.arange(1, 101)
margin_call_threshold = 100 / leverage_range # % move to wipe out account
axes[1].plot(leverage_range, margin_call_threshold, color='red', linewidth=2)
axes[1].fill_between(leverage_range, margin_call_threshold, 0, alpha=0.3, color='red')
axes[1].set_xlabel('Leverage Ratio')
axes[1].set_ylabel('Adverse Move to Wipe Out Account (%)')
axes[1].set_title('Margin Call Threshold by Leverage')
axes[1].grid(True, alpha=0.3)
# Add annotations
axes[1].annotate('50:1 = 2% move', xy=(50, 2), xytext=(60, 10),
arrowprops=dict(arrowstyle='->', color='black'),
fontsize=10)
plt.tight_layout()
plt.show()
OANDA API Preview
Throughout this course, we'll use the OANDA API for forex data and trading. Here's a preview of how it works.
# Mock OANDA-style data for demonstration
# In real use, you'll connect to OANDA's API
def generate_forex_data(
pair: str = 'EUR_USD',
days: int = 30,
start_price: float = 1.0850
) -> pd.DataFrame:
"""
Generate mock forex OHLC data.
Args:
pair: Currency pair
days: Number of days
start_price: Starting price
Returns:
DataFrame with OHLC data
"""
np.random.seed(42)
dates = pd.date_range(end=pd.Timestamp.today(), periods=days, freq='D')
# Generate price path
returns = np.random.normal(0, 0.005, days)
close_prices = start_price * np.cumprod(1 + returns)
# Generate OHLC from close
daily_volatility = 0.003
high_prices = close_prices * (1 + np.abs(np.random.normal(0, daily_volatility, days)))
low_prices = close_prices * (1 - np.abs(np.random.normal(0, daily_volatility, days)))
open_prices = np.roll(close_prices, 1)
open_prices[0] = start_price
df = pd.DataFrame({
'open': open_prices,
'high': high_prices,
'low': low_prices,
'close': close_prices,
'volume': np.random.randint(10000, 50000, days)
}, index=dates)
return df
# Generate sample data
eur_usd = generate_forex_data('EUR_USD', days=60)
print("EUR/USD Sample Data")
print("=" * 60)
print(eur_usd.tail(10).round(5))
# Visualize forex data
fig, axes = plt.subplots(2, 1, figsize=(12, 8), height_ratios=[3, 1])
# Candlestick-style visualization using bars
ax1 = axes[0]
colors = ['green' if row['close'] >= row['open'] else 'red'
for _, row in eur_usd.iterrows()]
# Plot high-low lines
for i, (idx, row) in enumerate(eur_usd.iterrows()):
ax1.plot([i, i], [row['low'], row['high']], color=colors[i], linewidth=1)
ax1.plot([i, i], [row['open'], row['close']], color=colors[i], linewidth=4)
ax1.set_ylabel('Price')
ax1.set_title('EUR/USD Price Chart')
ax1.grid(True, alpha=0.3)
# Set x-axis labels
tick_positions = range(0, len(eur_usd), 10)
tick_labels = [eur_usd.index[i].strftime('%m/%d') for i in tick_positions]
ax1.set_xticks(tick_positions)
ax1.set_xticklabels(tick_labels)
# Volume
ax2 = axes[1]
ax2.bar(range(len(eur_usd)), eur_usd['volume'], color=colors, alpha=0.7)
ax2.set_ylabel('Volume')
ax2.set_xlabel('Date')
ax2.set_xticks(tick_positions)
ax2.set_xticklabels(tick_labels)
ax2.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
Course Projects Overview
Each module ends with a hands-on project:
| Module | Project |
|---|---|
| 1 | Forex Market Analyzer |
| 2 | Futures Data Handler |
| 3 | Leverage Risk Calculator |
| 4 | Forex Data System |
| 5 | Technical Analysis Suite |
| 6 | Fundamental Dashboard |
| 7 | Commodity Trading System |
| 8 | Multi-Strategy Forex System |
| 9 | Risk Management System |
| 10 | Forex/Futures Backtester |
| 11 | Live Trading System |
| 12 | Advanced Strategy Toolkit |
| 13 | Production Monitoring System |
| 14 | Automated Trading Journal |
| Capstone | 24-Hour Forex/Futures Trading System |
Getting Started
Recommended Setup
- OANDA Demo Account
- Sign up at OANDA
- Create practice account for API access
-
No deposit required for demo trading
-
Install Required Packages
bash pip install oandapyV20 MetaTrader5 ta yfinance pandas numpy matplotlib -
Start Learning
- Begin with Module 1: Forex Market Basics
- Complete all exercises (3 guided + 3 open-ended per module)
- Build each module project
Tips for Success
- Start with demo accounts - Never trade real money while learning
- Respect leverage - It's a double-edged sword
- Understand costs - Spreads, swaps, and commissions add up
- Master risk management - Position sizing is critical in leveraged markets
- Practice 24h thinking - These markets never sleep
Key Takeaways
- Forex is the largest financial market with $7.5 trillion daily volume
- Futures provide standardized contracts for commodities, indices, and currencies
- Leverage amplifies both gains and losses - handle with care
- 24-hour markets require different operational approaches
- Economic events drive forex markets more than other asset classes
Next: Module 1 - Forex Market Basics
Module 1: Forex Market Basics
Part 1: Market Fundamentals
| Duration | Exercises |
|---|---|
| ~2.5 hours | 6 |
Learning Objectives
By the end of this module, you will be able to:
- Understand how currency pairs work (base vs quote currency)
- Identify major, minor, and exotic currency pairs
- Navigate the decentralized forex market structure
- Understand trading sessions and their characteristics
- Calculate pip values for different currency pairs
- Work with lot sizes and position sizing
# Standard imports for this module
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from datetime import datetime, timedelta, timezone
from typing import Dict, List, Tuple, Optional
# Display settings
pd.set_option('display.max_columns', 15)
pd.set_option('display.width', 200)
plt.style.use('seaborn-v0_8-whitegrid')
1.1 What is Forex?
The foreign exchange market (Forex or FX) is the global marketplace for trading currencies. It's the largest and most liquid financial market in the world, with a daily trading volume exceeding $7.5 trillion.
Currency Pairs Explained
In forex, currencies are always traded in pairs. When you trade EUR/USD, you're simultaneously: - Buying one currency (base currency) - Selling another currency (quote currency)
EUR/USD = 1.0850
│ │ │
│ │ └── Price: 1 EUR = 1.0850 USD
│ └── Quote Currency (what you pay)
└── Base Currency (what you buy)
# Understanding currency pairs
class CurrencyPair:
"""
Represents a forex currency pair.
Attributes:
base: Base currency code
quote: Quote currency code
price: Current exchange rate
"""
def __init__(self, base: str, quote: str, price: float):
self.base = base
self.quote = quote
self.price = price
@property
def symbol(self) -> str:
"""Return the pair symbol."""
return f"{self.base}/{self.quote}"
def convert(self, amount: float, direction: str = 'base_to_quote') -> float:
"""
Convert an amount between currencies.
Args:
amount: Amount to convert
direction: 'base_to_quote' or 'quote_to_base'
Returns:
Converted amount
"""
if direction == 'base_to_quote':
return amount * self.price
else:
return amount / self.price
def __repr__(self) -> str:
return f"CurrencyPair({self.symbol} = {self.price})"
# Example usage
eur_usd = CurrencyPair('EUR', 'USD', 1.0850)
print(f"Pair: {eur_usd.symbol}")
print(f"Rate: {eur_usd.price}")
print(f"\n10,000 EUR = {eur_usd.convert(10000):.2f} USD")
print(f"10,000 USD = {eur_usd.convert(10000, 'quote_to_base'):.2f} EUR")
Major, Minor, and Exotic Pairs
Currency pairs are categorized based on trading volume and liquidity:
| Category | Description | Examples | Spread |
|---|---|---|---|
| Major | USD paired with major economies | EUR/USD, GBP/USD, USD/JPY | Tightest (0.5-2 pips) |
| Minor | Major currencies without USD | EUR/GBP, EUR/JPY, GBP/JPY | Moderate (2-5 pips) |
| Exotic | Major + emerging market currency | USD/TRY, EUR/ZAR, USD/MXN | Widest (5-50+ pips) |
# Currency pair classifications
MAJOR_PAIRS = {
'EUR/USD': {'name': 'Euro', 'nickname': 'Fiber'},
'GBP/USD': {'name': 'British Pound', 'nickname': 'Cable'},
'USD/JPY': {'name': 'Japanese Yen', 'nickname': 'Gopher'},
'USD/CHF': {'name': 'Swiss Franc', 'nickname': 'Swissie'},
'AUD/USD': {'name': 'Australian Dollar', 'nickname': 'Aussie'},
'USD/CAD': {'name': 'Canadian Dollar', 'nickname': 'Loonie'},
'NZD/USD': {'name': 'New Zealand Dollar', 'nickname': 'Kiwi'}
}
MINOR_PAIRS = [
'EUR/GBP', 'EUR/JPY', 'EUR/CHF', 'EUR/AUD', 'EUR/CAD', 'EUR/NZD',
'GBP/JPY', 'GBP/CHF', 'GBP/AUD', 'GBP/CAD', 'GBP/NZD',
'AUD/JPY', 'AUD/CHF', 'AUD/CAD', 'AUD/NZD',
'CAD/JPY', 'CAD/CHF', 'CHF/JPY', 'NZD/JPY'
]
EXOTIC_PAIRS = [
'USD/TRY', 'USD/ZAR', 'USD/MXN', 'USD/SGD', 'USD/HKD',
'USD/SEK', 'USD/NOK', 'USD/DKK', 'USD/PLN', 'USD/HUF',
'EUR/TRY', 'EUR/ZAR', 'EUR/NOK', 'EUR/SEK', 'EUR/PLN'
]
print("Major Pairs (USD-based, highest liquidity)")
print("=" * 55)
for pair, info in MAJOR_PAIRS.items():
print(f"{pair:10} - {info['name']:25} ({info['nickname']})")
print(f"\nMinor Pairs (Cross pairs without USD): {len(MINOR_PAIRS)} pairs")
print(f"Exotic Pairs (Emerging market currencies): {len(EXOTIC_PAIRS)} pairs")
def classify_pair(pair: str) -> str:
"""
Classify a currency pair as major, minor, or exotic.
Args:
pair: Currency pair symbol (e.g., 'EUR/USD')
Returns:
Classification string
"""
pair = pair.upper().replace('_', '/')
if pair in MAJOR_PAIRS:
return 'Major'
elif pair in MINOR_PAIRS:
return 'Minor'
elif pair in EXOTIC_PAIRS:
return 'Exotic'
else:
# Check if it could be exotic based on currency codes
exotic_currencies = ['TRY', 'ZAR', 'MXN', 'SGD', 'HKD', 'SEK', 'NOK',
'DKK', 'PLN', 'HUF', 'CZK', 'RUB', 'BRL', 'INR']
base, quote = pair.split('/')
if base in exotic_currencies or quote in exotic_currencies:
return 'Exotic'
return 'Unknown'
# Test classification
test_pairs = ['EUR/USD', 'GBP/JPY', 'USD/TRY', 'AUD/NZD', 'EUR/ZAR']
print("Pair Classification")
print("=" * 30)
for pair in test_pairs:
print(f"{pair:12} -> {classify_pair(pair)}")
1.2 Market Structure
Decentralized Market
Unlike stock exchanges, forex has no central exchange. It operates as an Over-The-Counter (OTC) market where trading happens electronically between participants worldwide.
Market Hierarchy
┌─────────────────────┐
│ Interbank Market │ ← Largest banks trade directly
│ (Tier 1 Liquidity) │
└──────────┬──────────┘
│
┌────────────────┼────────────────┐
│ │ │
┌────────┴────────┐ ┌────┴────┐ ┌────────┴────────┐
│ Prime Brokers │ │ ECNs │ │ Market Makers │
│ (Hedge Funds) │ │ │ │ (Retail Flow) │
└────────┬────────┘ └────┬────┘ └────────┬────────┘
│ │ │
└───────────────┼───────────────┘
│
┌─────────┴─────────┐
│ Retail Brokers │
│ (Your Broker) │
└─────────┬─────────┘
│
┌─────────┴─────────┐
│ Retail Traders │ ← You
└───────────────────┘
# Market participants and their characteristics
MARKET_PARTICIPANTS = {
'Central Banks': {
'role': 'Monetary policy, currency intervention',
'volume': 'Massive but infrequent',
'examples': ['Federal Reserve', 'ECB', 'BOJ', 'BOE']
},
'Commercial Banks': {
'role': 'Interbank trading, customer orders',
'volume': 'Very high (Tier 1 liquidity)',
'examples': ['JP Morgan', 'Citi', 'Deutsche Bank', 'UBS']
},
'Investment Banks': {
'role': 'Proprietary trading, market making',
'volume': 'High',
'examples': ['Goldman Sachs', 'Morgan Stanley']
},
'Hedge Funds': {
'role': 'Speculative trading, arbitrage',
'volume': 'High',
'examples': ['Bridgewater', 'Citadel', 'Renaissance']
},
'Corporations': {
'role': 'Hedging currency exposure',
'volume': 'Moderate',
'examples': ['Apple', 'Toyota', 'Nestlé']
},
'Retail Traders': {
'role': 'Speculation',
'volume': 'Low (5-10% of market)',
'examples': ['Individual traders via brokers']
}
}
print("Forex Market Participants")
print("=" * 70)
for participant, info in MARKET_PARTICIPANTS.items():
print(f"\n{participant}")
print(f" Role: {info['role']}")
print(f" Volume: {info['volume']}")
print(f" Examples: {', '.join(info['examples'])}")
# Market share by participant type (approximate)
participant_shares = {
'Commercial Banks': 42,
'Investment Banks': 18,
'Hedge Funds': 12,
'Central Banks': 8,
'Corporations': 8,
'Retail Traders': 7,
'Other': 5
}
# Visualize market share
fig, ax = plt.subplots(figsize=(10, 6))
colors = plt.cm.Set3(np.linspace(0, 1, len(participant_shares)))
wedges, texts, autotexts = ax.pie(
participant_shares.values(),
labels=participant_shares.keys(),
autopct='%1.0f%%',
colors=colors,
explode=[0.02] * len(participant_shares),
shadow=True
)
ax.set_title('Forex Market Share by Participant Type', fontsize=14, fontweight='bold')
plt.tight_layout()
plt.show()
Exercise 1.1: Identify Currency Pair Types (Guided)
Complete the function to identify whether a currency pair is major, minor, or exotic.
Solution 1.1
def identify_pair_type(pair: str) -> dict:
"""
Identify the type and characteristics of a currency pair.
Args:
pair: Currency pair symbol (e.g., 'EUR/USD')
Returns:
Dictionary with pair type and characteristics
"""
# Normalize the pair format
pair = pair.upper().replace('_', '/') # Convert to uppercase
# Split into base and quote currencies
base, quote = pair.split('/') # Split by delimiter
# Define major currencies
major_currencies = ['USD', 'EUR', 'GBP', 'JPY', 'CHF', 'AUD', 'CAD', 'NZD']
# Determine pair type
has_usd = 'USD' in pair # Check if USD is in the pair
both_major = base in major_currencies and quote in major_currencies
if has_usd and both_major:
pair_type = 'Major'
typical_spread = '0.5-2 pips'
elif both_major and not has_usd:
pair_type = 'Minor'
typical_spread = '2-5 pips'
else:
pair_type = 'Exotic'
typical_spread = '5-50+ pips'
return {
'pair': pair,
'base': base,
'quote': quote,
'type': pair_type,
'typical_spread': typical_spread
}
1.3 Trading Sessions
The forex market operates 24 hours a day, 5 days a week. Trading activity rotates through four major sessions as the sun moves around the globe.
Session Times (UTC)
| Session | Open (UTC) | Close (UTC) | Key Pairs | Characteristics |
|---|---|---|---|---|
| Sydney | 21:00 | 06:00 | AUD, NZD | Low volatility start |
| Tokyo | 00:00 | 09:00 | JPY pairs | Moderate activity |
| London | 08:00 | 17:00 | EUR, GBP | Highest volume |
| New York | 13:00 | 22:00 | USD pairs | High volatility |
class ForexSession:
"""
Represents a forex trading session.
Attributes:
name: Session name
open_hour: Opening hour in UTC
close_hour: Closing hour in UTC
key_currencies: Most active currencies during session
"""
def __init__(self, name: str, open_hour: int, close_hour: int,
key_currencies: List[str]):
self.name = name
self.open_hour = open_hour
self.close_hour = close_hour
self.key_currencies = key_currencies
def is_open(self, utc_hour: int) -> bool:
"""Check if the session is open at a given UTC hour."""
if self.open_hour < self.close_hour:
return self.open_hour <= utc_hour < self.close_hour
else: # Crosses midnight
return utc_hour >= self.open_hour or utc_hour < self.close_hour
def hours_until_open(self, utc_hour: int) -> int:
"""Calculate hours until session opens."""
if self.is_open(utc_hour):
return 0
hours = (self.open_hour - utc_hour) % 24
return hours
# Define the four major sessions
SESSIONS = {
'Sydney': ForexSession('Sydney', 21, 6, ['AUD', 'NZD']),
'Tokyo': ForexSession('Tokyo', 0, 9, ['JPY', 'AUD']),
'London': ForexSession('London', 8, 17, ['EUR', 'GBP', 'CHF']),
'New York': ForexSession('New York', 13, 22, ['USD', 'CAD'])
}
# Check current session status
current_utc_hour = datetime.now(timezone.utc).hour
print(f"Current UTC Hour: {current_utc_hour:02d}:00")
print("\nSession Status")
print("=" * 50)
for name, session in SESSIONS.items():
status = "OPEN" if session.is_open(current_utc_hour) else "CLOSED"
hours_until = session.hours_until_open(current_utc_hour)
hours_info = f"" if hours_until == 0 else f" (opens in {hours_until}h)"
currencies = ', '.join(session.key_currencies)
print(f"{name:12} {status:6}{hours_info:15} [{currencies}]")
# Visualize session overlaps
def plot_forex_sessions():
"""Create a visual representation of forex trading sessions."""
fig, ax = plt.subplots(figsize=(14, 5))
sessions_data = [
('Sydney', 21, 6, '#2ecc71'),
('Tokyo', 0, 9, '#e74c3c'),
('London', 8, 17, '#3498db'),
('New York', 13, 22, '#9b59b6')
]
for i, (name, start, end, color) in enumerate(sessions_data):
y = i * 0.8
if start > end: # Crosses midnight
# Draw first part (start to midnight)
ax.barh(y, 24 - start, left=start, height=0.6, color=color, alpha=0.7)
# Draw second part (midnight to end)
ax.barh(y, end, left=0, height=0.6, color=color, alpha=0.7)
else:
ax.barh(y, end - start, left=start, height=0.6, color=color, alpha=0.7)
ax.text(-0.5, y, name, ha='right', va='center', fontweight='bold', fontsize=11)
# Highlight overlaps
ax.axvspan(8, 9, alpha=0.2, color='orange', label='Tokyo-London Overlap')
ax.axvspan(13, 17, alpha=0.2, color='red', label='London-NY Overlap')
ax.set_xlim(-4, 24)
ax.set_ylim(-0.5, 3.5)
ax.set_xlabel('UTC Hour', fontsize=12)
ax.set_xticks(range(0, 25, 2))
ax.set_xticklabels([f'{h:02d}:00' for h in range(0, 25, 2)])
ax.set_yticks([])
ax.set_title('Forex Trading Sessions (UTC)', fontsize=14, fontweight='bold')
ax.legend(loc='upper right')
ax.grid(True, axis='x', alpha=0.3)
plt.tight_layout()
plt.show()
plot_forex_sessions()
# Session volatility analysis
def estimate_session_volatility(pair: str, session: str) -> dict:
"""
Estimate typical volatility for a pair during a session.
Args:
pair: Currency pair
session: Trading session name
Returns:
Dictionary with volatility estimates
"""
# Volatility multipliers (base = London session)
session_multipliers = {
'Sydney': 0.5,
'Tokyo': 0.7,
'London': 1.0,
'New York': 0.9,
'London-NY Overlap': 1.2
}
# Base pip ranges per pair (approximate daily range in London)
base_ranges = {
'EUR/USD': 60,
'GBP/USD': 90,
'USD/JPY': 70,
'AUD/USD': 65,
'USD/CAD': 70
}
pair = pair.upper().replace('_', '/')
base_range = base_ranges.get(pair, 75)
multiplier = session_multipliers.get(session, 1.0)
session_range = base_range * multiplier
return {
'pair': pair,
'session': session,
'expected_range_pips': round(session_range),
'volatility_level': 'High' if multiplier >= 1.0 else 'Medium' if multiplier >= 0.7 else 'Low'
}
# Compare volatility across sessions
print("EUR/USD Volatility by Session")
print("=" * 50)
for session in ['Sydney', 'Tokyo', 'London', 'New York', 'London-NY Overlap']:
vol = estimate_session_volatility('EUR/USD', session)
print(f"{session:20} {vol['expected_range_pips']:3} pips ({vol['volatility_level']})")
Exercise 1.2: Session Analysis (Guided)
Complete the function to determine active sessions for a given UTC time.
Solution 1.2
def get_active_sessions(utc_hour: int) -> dict:
"""
Determine which forex sessions are active at a given UTC hour.
Args:
utc_hour: Hour in UTC (0-23)
Returns:
Dictionary with active sessions and trading conditions
"""
sessions_info = {
'Sydney': (21, 6), # 21:00 - 06:00
'Tokyo': (0, 9), # 00:00 - 09:00
'London': (8, 17), # 08:00 - 17:00
'New York': (13, 22) # 13:00 - 22:00
}
active = []
for session, (open_h, close_h) in sessions_info.items(): # Iterate over items
if open_h > close_h: # Session crosses midnight
is_open = utc_hour >= open_h or utc_hour < close_h # Logical OR
else:
is_open = open_h <= utc_hour < close_h
if is_open:
active.append(session) # Add to list
# Determine trading conditions
num_active = len(active)
if num_active >= 2:
condition = 'High Volatility (Session Overlap)'
elif num_active == 1:
condition = 'Normal Volatility'
else:
condition = 'Low Volatility (Weekend)'
return {
'utc_hour': utc_hour,
'active_sessions': active,
'num_sessions': num_active,
'condition': condition
}
1.4 Pips & Lots
What is a Pip?
A pip (Percentage in Point) is the smallest price movement in forex:
- For most pairs: 0.0001 (fourth decimal place)
- For JPY pairs: 0.01 (second decimal place)
EUR/USD: 1.0850 → 1.0851 = +1 pip
USD/JPY: 150.00 → 150.01 = +1 pip
Lot Sizes
| Lot Type | Units | EUR/USD Pip Value |
|---|---|---|
| Standard | 100,000 | $10 per pip |
| Mini | 10,000 | $1 per pip |
| Micro | 1,000 | $0.10 per pip |
| Nano | 100 | $0.01 per pip |
class PipCalculator:
"""
Calculator for pip values and position sizing.
Attributes:
pair: Currency pair
account_currency: Account denomination currency
"""
# Pip sizes for different pair types
STANDARD_PIP = 0.0001
JPY_PIP = 0.01
# Lot sizes
LOT_SIZES = {
'standard': 100_000,
'mini': 10_000,
'micro': 1_000,
'nano': 100
}
def __init__(self, pair: str, account_currency: str = 'USD'):
self.pair = pair.upper().replace('_', '/')
self.account_currency = account_currency.upper()
self.base, self.quote = self.pair.split('/')
@property
def pip_size(self) -> float:
"""Get the pip size for this pair."""
return self.JPY_PIP if 'JPY' in self.pair else self.STANDARD_PIP
def pip_value(self, lot_size: float = 1.0, exchange_rate: Optional[float] = None) -> float:
"""
Calculate pip value in account currency.
Args:
lot_size: Position size in lots
exchange_rate: Current exchange rate (for conversion if needed)
Returns:
Pip value in account currency
"""
units = lot_size * self.LOT_SIZES['standard']
pip_value_in_quote = units * self.pip_size
# If quote currency matches account currency, we're done
if self.quote == self.account_currency:
return pip_value_in_quote
# Otherwise, need to convert (simplified - assumes USD account)
if exchange_rate is not None:
if self.account_currency == 'USD' and self.base == 'USD':
return pip_value_in_quote / exchange_rate
return pip_value_in_quote
def pips_to_price(self, pips: int) -> float:
"""Convert pips to price movement."""
return pips * self.pip_size
def price_to_pips(self, price_change: float) -> float:
"""Convert price movement to pips."""
return price_change / self.pip_size
# Example calculations
calc = PipCalculator('EUR/USD')
print("EUR/USD Pip Calculations")
print("=" * 40)
print(f"Pip size: {calc.pip_size}")
print(f"1 standard lot pip value: ${calc.pip_value(1.0):.2f}")
print(f"1 mini lot pip value: ${calc.pip_value(0.1):.2f}")
print(f"1 micro lot pip value: ${calc.pip_value(0.01):.2f}")
print(f"\n50 pips = {calc.pips_to_price(50):.4f} price movement")
print("\n" + "=" * 40)
calc_jpy = PipCalculator('USD/JPY')
print(f"USD/JPY pip size: {calc_jpy.pip_size}")
print(f"50 pips = {calc_jpy.pips_to_price(50):.2f} price movement")
# Pip value comparison across pairs
def compare_pip_values(pairs: List[str], lot_size: float = 1.0) -> pd.DataFrame:
"""
Compare pip values across different currency pairs.
Args:
pairs: List of currency pairs
lot_size: Position size in lots
Returns:
DataFrame with pip value comparison
"""
results = []
for pair in pairs:
calc = PipCalculator(pair)
pip_val = calc.pip_value(lot_size)
results.append({
'Pair': pair,
'Pip Size': calc.pip_size,
f'Pip Value ({lot_size} lot)': f'${pip_val:.2f}',
'10 Pip Move': f'${pip_val * 10:.2f}',
'50 Pip Move': f'${pip_val * 50:.2f}'
})
return pd.DataFrame(results)
# Compare major pairs
pairs = ['EUR/USD', 'GBP/USD', 'USD/JPY', 'AUD/USD', 'USD/CAD']
print("Pip Value Comparison (1 Standard Lot)")
print("=" * 70)
print(compare_pip_values(pairs).to_string(index=False))
Exercise 1.3: Calculate Pip Values (Guided)
Complete the function to calculate profit/loss in pips and monetary value.
Solution 1.3
def calculate_trade_pnl(
pair: str,
entry_price: float,
exit_price: float,
lot_size: float,
direction: str # 'long' or 'short'
) -> dict:
"""
Calculate profit/loss for a forex trade.
Args:
pair: Currency pair
entry_price: Entry price
exit_price: Exit price
lot_size: Position size in lots
direction: Trade direction ('long' or 'short')
Returns:
Dictionary with P&L details
"""
calc = PipCalculator(pair)
# Calculate price change
price_change = exit_price - entry_price
# Adjust for direction
if direction.lower() == 'short': # Short position
price_change = -price_change
# Convert to pips
pips_gained = calc.price_to_pips(price_change) # Convert price to pips
# Calculate monetary P&L
pip_value = calc.pip_value(lot_size)
monetary_pnl = pips_gained * pip_value # Multiply pips by pip value
return {
'pair': pair,
'direction': direction,
'entry': entry_price,
'exit': exit_price,
'lots': lot_size,
'pips': round(pips_gained, 1),
'pnl': round(monetary_pnl, 2)
}
Exercise 1.4: Build Currency Pair Analyzer (Open-ended)
Build a comprehensive currency pair analyzer class that includes: - Pair classification (major/minor/exotic) - Pip calculations - Spread cost analysis - Position sizing based on risk
Your implementation:
Solution 1.4
class CurrencyPairAnalyzer:
"""
Comprehensive analyzer for forex currency pairs.
Attributes:
pair: Currency pair symbol
base: Base currency
quote: Quote currency
"""
MAJOR_CURRENCIES = ['USD', 'EUR', 'GBP', 'JPY', 'CHF', 'AUD', 'CAD', 'NZD']
EXOTIC_CURRENCIES = ['TRY', 'ZAR', 'MXN', 'SGD', 'HKD', 'SEK', 'NOK', 'PLN']
TYPICAL_SPREADS = {
'Major': 1.0,
'Minor': 3.0,
'Exotic': 15.0
}
def __init__(self, pair: str):
self.pair = pair.upper().replace('_', '/')
self.base, self.quote = self.pair.split('/')
self._pip_size = 0.01 if 'JPY' in self.pair else 0.0001
def classify(self) -> str:
"""Classify the pair as major, minor, or exotic."""
has_usd = 'USD' in self.pair
base_major = self.base in self.MAJOR_CURRENCIES
quote_major = self.quote in self.MAJOR_CURRENCIES
if has_usd and base_major and quote_major:
return 'Major'
elif base_major and quote_major:
return 'Minor'
else:
return 'Exotic'
def pip_value(self, lots: float = 1.0) -> float:
"""Calculate pip value in USD."""
units = lots * 100_000
return units * self._pip_size
def spread_cost(self, lots: float, spread_pips: float = None) -> float:
"""Calculate spread cost for a position."""
if spread_pips is None:
spread_pips = self.TYPICAL_SPREADS[self.classify()]
return self.pip_value(lots) * spread_pips
def position_size(
self,
account_balance: float,
risk_pct: float,
stop_loss_pips: int
) -> float:
"""Calculate position size based on risk parameters."""
risk_amount = account_balance * (risk_pct / 100)
pip_value_per_lot = self.pip_value(1.0)
risk_per_lot = pip_value_per_lot * stop_loss_pips
return round(risk_amount / risk_per_lot, 2)
def summary(self) -> dict:
"""Get a comprehensive summary of the pair."""
return {
'pair': self.pair,
'base': self.base,
'quote': self.quote,
'classification': self.classify(),
'pip_size': self._pip_size,
'pip_value_std_lot': f'${self.pip_value(1.0):.2f}',
'typical_spread': f'{self.TYPICAL_SPREADS[self.classify()]} pips',
'spread_cost_std_lot': f'${self.spread_cost(1.0):.2f}'
}
# Test the analyzer
for pair in ['EUR/USD', 'GBP/JPY', 'USD/TRY']:
analyzer = CurrencyPairAnalyzer(pair)
summary = analyzer.summary()
print(f"\n{pair} Analysis:")
for key, value in summary.items():
print(f" {key}: {value}")
# Position sizing example
lots = analyzer.position_size(10000, 2, 50)
print(f" Position size ($10k, 2% risk, 50 pip SL): {lots} lots")
Exercise 1.5: Session-Based Trading Filter (Open-ended)
Build a session-based trading filter that determines optimal trading times for different currency pairs based on active sessions.
Your implementation:
Solution 1.5
class SessionTradingFilter:
"""
Filter trading times based on forex sessions.
Helps identify optimal trading windows for different pairs.
"""
SESSIONS = {
'Sydney': {'open': 21, 'close': 6, 'currencies': ['AUD', 'NZD']},
'Tokyo': {'open': 0, 'close': 9, 'currencies': ['JPY', 'AUD']},
'London': {'open': 8, 'close': 17, 'currencies': ['EUR', 'GBP', 'CHF']},
'New York': {'open': 13, 'close': 22, 'currencies': ['USD', 'CAD']}
}
def __init__(self):
pass
def is_session_open(self, session_name: str, utc_hour: int) -> bool:
"""Check if a session is open at a given UTC hour."""
session = self.SESSIONS.get(session_name)
if not session:
return False
open_h, close_h = session['open'], session['close']
if open_h > close_h: # Crosses midnight
return utc_hour >= open_h or utc_hour < close_h
return open_h <= utc_hour < close_h
def get_active_sessions(self, utc_hour: int) -> List[str]:
"""Get all active sessions at a given UTC hour."""
return [
name for name in self.SESSIONS
if self.is_session_open(name, utc_hour)
]
def _get_pair_sessions(self, pair: str) -> List[str]:
"""Get relevant sessions for a currency pair."""
pair = pair.upper().replace('_', '/')
base, quote = pair.split('/')
relevant = []
for session_name, info in self.SESSIONS.items():
if base in info['currencies'] or quote in info['currencies']:
relevant.append(session_name)
return relevant
def is_good_time_to_trade(self, pair: str, utc_hour: int) -> dict:
"""Determine if it's a good time to trade a pair."""
active = self.get_active_sessions(utc_hour)
relevant = self._get_pair_sessions(pair)
# Check if relevant sessions are active
matching = [s for s in relevant if s in active]
if len(matching) >= 2:
rating = 'Excellent'
reason = 'Multiple relevant sessions overlap'
elif len(matching) == 1:
rating = 'Good'
reason = f'{matching[0]} session active'
elif len(active) > 0:
rating = 'Fair'
reason = 'Active sessions but not optimal for this pair'
else:
rating = 'Poor'
reason = 'No major sessions active'
return {
'pair': pair,
'utc_hour': utc_hour,
'rating': rating,
'reason': reason,
'active_sessions': active,
'relevant_sessions': relevant
}
def get_optimal_hours(self, pair: str) -> List[int]:
"""Get optimal trading hours for a pair."""
optimal = []
for hour in range(24):
result = self.is_good_time_to_trade(pair, hour)
if result['rating'] in ['Excellent', 'Good']:
optimal.append(hour)
return optimal
# Test the filter
filter = SessionTradingFilter()
# Check current conditions
current_hour = datetime.now(timezone.utc).hour
for pair in ['EUR/USD', 'AUD/JPY', 'GBP/USD']:
result = filter.is_good_time_to_trade(pair, current_hour)
print(f"\n{pair} at {current_hour}:00 UTC:")
print(f" Rating: {result['rating']}")
print(f" Reason: {result['reason']}")
print(f" Optimal hours: {filter.get_optimal_hours(pair)}")
Exercise 1.6: Multi-Pair Market Scanner (Open-ended)
Build a market scanner that analyzes multiple currency pairs and ranks them by trading opportunity.
Your implementation:
Solution 1.6
class ForexMarketScanner:
"""
Scanner for analyzing multiple forex pairs.
Ranks pairs by opportunity based on volatility and session.
"""
def __init__(self):
self.pairs = {}
self.session_filter = SessionTradingFilter()
def add_pair(self, pair: str, current_price: float, daily_range: float):
"""Add a pair with its current market data."""
pair = pair.upper().replace('_', '/')
self.pairs[pair] = {
'price': current_price,
'daily_range': daily_range,
'analyzer': CurrencyPairAnalyzer(pair)
}
def analyze_pair(self, pair: str, utc_hour: int) -> dict:
"""Analyze a single pair for trading opportunity."""
pair = pair.upper().replace('_', '/')
if pair not in self.pairs:
return {'error': 'Pair not found'}
data = self.pairs[pair]
analyzer = data['analyzer']
# Get session rating
session_info = self.session_filter.is_good_time_to_trade(pair, utc_hour)
# Calculate volatility score (daily range in pips)
pip_size = 0.01 if 'JPY' in pair else 0.0001
range_pips = data['daily_range'] / pip_size
# Score calculation
session_scores = {'Excellent': 4, 'Good': 3, 'Fair': 2, 'Poor': 1}
session_score = session_scores.get(session_info['rating'], 1)
# Normalize volatility (assume 100 pips is high)
volatility_score = min(range_pips / 25, 4) # Max 4 points
# Classification bonus
class_bonus = {'Major': 1.0, 'Minor': 0.8, 'Exotic': 0.6}
total_score = (session_score + volatility_score) * class_bonus.get(
analyzer.classify(), 1.0
)
return {
'pair': pair,
'classification': analyzer.classify(),
'price': data['price'],
'daily_range_pips': round(range_pips, 1),
'session_rating': session_info['rating'],
'opportunity_score': round(total_score, 2),
'spread_cost': analyzer.spread_cost(1.0)
}
def scan_all(self, utc_hour: int) -> pd.DataFrame:
"""Scan all pairs and return ranked results."""
results = []
for pair in self.pairs:
analysis = self.analyze_pair(pair, utc_hour)
if 'error' not in analysis:
results.append(analysis)
df = pd.DataFrame(results)
if not df.empty:
df = df.sort_values('opportunity_score', ascending=False)
return df
def get_top_opportunities(self, utc_hour: int, n: int = 5) -> List[str]:
"""Get top N pairs by opportunity."""
df = self.scan_all(utc_hour)
if df.empty:
return []
return df.head(n)['pair'].tolist()
# Test the scanner
scanner = ForexMarketScanner()
# Add sample pairs with mock data
pairs_data = [
('EUR/USD', 1.0850, 0.0065),
('GBP/USD', 1.2650, 0.0095),
('USD/JPY', 150.50, 0.85),
('AUD/USD', 0.6550, 0.0055),
('EUR/GBP', 0.8580, 0.0045),
('USD/CAD', 1.3650, 0.0070),
('USD/TRY', 32.50, 1.25)
]
for pair, price, range_val in pairs_data:
scanner.add_pair(pair, price, range_val)
# Scan at current time
current_hour = datetime.now(timezone.utc).hour
print(f"\nMarket Scan at {current_hour}:00 UTC")
print("=" * 80)
results = scanner.scan_all(current_hour)
print(results.to_string(index=False))
print(f"\nTop Opportunities: {scanner.get_top_opportunities(current_hour, 3)}")
Module Project: Forex Market Analyzer
Build a comprehensive forex market analyzer that combines all concepts from this module.
class ForexMarketAnalyzer:
"""
Comprehensive forex market analyzer.
Combines pair analysis, session tracking, and pip calculations
into a unified interface for forex market analysis.
Attributes:
account_currency: Base currency for calculations
account_balance: Trading account balance
"""
MAJOR_CURRENCIES = ['USD', 'EUR', 'GBP', 'JPY', 'CHF', 'AUD', 'CAD', 'NZD']
SESSIONS = {
'Sydney': {'open': 21, 'close': 6, 'currencies': ['AUD', 'NZD']},
'Tokyo': {'open': 0, 'close': 9, 'currencies': ['JPY', 'AUD', 'NZD']},
'London': {'open': 8, 'close': 17, 'currencies': ['EUR', 'GBP', 'CHF']},
'New York': {'open': 13, 'close': 22, 'currencies': ['USD', 'CAD']}
}
TYPICAL_SPREADS = {'Major': 1.0, 'Minor': 3.0, 'Exotic': 15.0}
def __init__(self, account_currency: str = 'USD', account_balance: float = 10000):
self.account_currency = account_currency.upper()
self.account_balance = account_balance
self.watched_pairs = {}
# ==================== Pair Analysis ====================
def add_pair(self, pair: str, bid: float = None, ask: float = None):
"""
Add a currency pair to the watchlist.
Args:
pair: Currency pair symbol
bid: Current bid price
ask: Current ask price
"""
pair = self._normalize_pair(pair)
base, quote = pair.split('/')
self.watched_pairs[pair] = {
'base': base,
'quote': quote,
'bid': bid,
'ask': ask,
'classification': self._classify_pair(pair)
}
def _normalize_pair(self, pair: str) -> str:
"""Normalize pair format to BASE/QUOTE."""
return pair.upper().replace('_', '/')
def _classify_pair(self, pair: str) -> str:
"""Classify pair as Major, Minor, or Exotic."""
base, quote = pair.split('/')
has_usd = 'USD' in pair
both_major = base in self.MAJOR_CURRENCIES and quote in self.MAJOR_CURRENCIES
if has_usd and both_major:
return 'Major'
elif both_major:
return 'Minor'
return 'Exotic'
def get_pair_info(self, pair: str) -> dict:
"""Get comprehensive information about a pair."""
pair = self._normalize_pair(pair)
if pair not in self.watched_pairs:
self.add_pair(pair)
info = self.watched_pairs[pair].copy()
info['pip_size'] = self._get_pip_size(pair)
info['pip_value_std_lot'] = self._calculate_pip_value(pair, 1.0)
info['typical_spread'] = self.TYPICAL_SPREADS[info['classification']]
if info['bid'] and info['ask']:
info['current_spread'] = (info['ask'] - info['bid']) / info['pip_size']
return info
# ==================== Pip Calculations ====================
def _get_pip_size(self, pair: str) -> float:
"""Get pip size for a pair."""
return 0.01 if 'JPY' in pair else 0.0001
def _calculate_pip_value(self, pair: str, lots: float) -> float:
"""Calculate pip value in account currency."""
pip_size = self._get_pip_size(pair)
units = lots * 100_000
return units * pip_size
def calculate_pips(self, pair: str, entry: float, exit: float) -> float:
"""Calculate pip difference between two prices."""
pair = self._normalize_pair(pair)
pip_size = self._get_pip_size(pair)
return (exit - entry) / pip_size
def calculate_pnl(
self,
pair: str,
entry: float,
exit: float,
lots: float,
direction: str = 'long'
) -> dict:
"""Calculate P&L for a trade."""
pair = self._normalize_pair(pair)
# Calculate pip change
pips = self.calculate_pips(pair, entry, exit)
if direction.lower() == 'short':
pips = -pips
# Calculate monetary P&L
pip_value = self._calculate_pip_value(pair, lots)
pnl = pips * pip_value / 100_000 * lots * 100_000
# Simplified: pip_value for 1 lot * pips
pip_value_per_lot = self._calculate_pip_value(pair, 1.0)
pnl = pips * (pip_value_per_lot / (1 / self._get_pip_size(pair))) * lots
# Correct calculation
pnl = pips * lots * 10 # $10 per pip per lot for most pairs
return {
'pair': pair,
'direction': direction,
'entry': entry,
'exit': exit,
'lots': lots,
'pips': round(pips, 1),
'pnl': round(pnl, 2)
}
# ==================== Position Sizing ====================
def calculate_position_size(
self,
pair: str,
stop_loss_pips: int,
risk_percent: float = 1.0
) -> dict:
"""
Calculate position size based on risk parameters.
Args:
pair: Currency pair
stop_loss_pips: Stop loss distance in pips
risk_percent: Percentage of account to risk
Returns:
Position sizing details
"""
pair = self._normalize_pair(pair)
# Calculate risk amount
risk_amount = self.account_balance * (risk_percent / 100)
# Pip value per standard lot (approximately $10 for most pairs)
pip_value_per_lot = 10 # Simplified
# Risk per lot = pip_value * stop_loss_pips
risk_per_lot = pip_value_per_lot * stop_loss_pips
# Position size
lots = risk_amount / risk_per_lot
return {
'pair': pair,
'account_balance': self.account_balance,
'risk_percent': risk_percent,
'risk_amount': round(risk_amount, 2),
'stop_loss_pips': stop_loss_pips,
'position_lots': round(lots, 2),
'position_units': int(lots * 100_000)
}
# ==================== Session Analysis ====================
def _is_session_open(self, session_name: str, utc_hour: int) -> bool:
"""Check if a session is open."""
session = self.SESSIONS.get(session_name)
if not session:
return False
open_h, close_h = session['open'], session['close']
if open_h > close_h:
return utc_hour >= open_h or utc_hour < close_h
return open_h <= utc_hour < close_h
def get_active_sessions(self, utc_hour: int = None) -> List[str]:
"""Get currently active sessions."""
if utc_hour is None:
utc_hour = datetime.now(timezone.utc).hour
return [
name for name in self.SESSIONS
if self._is_session_open(name, utc_hour)
]
def analyze_trading_conditions(self, pair: str, utc_hour: int = None) -> dict:
"""
Analyze current trading conditions for a pair.
Args:
pair: Currency pair
utc_hour: Hour in UTC (default: current)
Returns:
Trading condition analysis
"""
if utc_hour is None:
utc_hour = datetime.now(timezone.utc).hour
pair = self._normalize_pair(pair)
base, quote = pair.split('/')
# Get active sessions
active = self.get_active_sessions(utc_hour)
# Find relevant sessions for this pair
relevant = []
for session_name, info in self.SESSIONS.items():
if base in info['currencies'] or quote in info['currencies']:
relevant.append(session_name)
# Determine trading condition
matching = [s for s in relevant if s in active]
if len(matching) >= 2:
rating = 'Excellent'
expected_volatility = 'High'
elif len(matching) == 1:
rating = 'Good'
expected_volatility = 'Medium-High'
elif len(active) > 0:
rating = 'Fair'
expected_volatility = 'Medium'
else:
rating = 'Poor'
expected_volatility = 'Low'
return {
'pair': pair,
'utc_hour': utc_hour,
'active_sessions': active,
'relevant_sessions': relevant,
'matching_sessions': matching,
'trading_rating': rating,
'expected_volatility': expected_volatility
}
# ==================== Market Overview ====================
def get_market_overview(self, utc_hour: int = None) -> pd.DataFrame:
"""Get overview of all watched pairs."""
if utc_hour is None:
utc_hour = datetime.now(timezone.utc).hour
results = []
for pair in self.watched_pairs:
info = self.get_pair_info(pair)
conditions = self.analyze_trading_conditions(pair, utc_hour)
results.append({
'Pair': pair,
'Type': info['classification'],
'Pip Value': f"${info['pip_value_std_lot']:.2f}",
'Typ. Spread': f"{info['typical_spread']} pips",
'Rating': conditions['trading_rating'],
'Volatility': conditions['expected_volatility']
})
return pd.DataFrame(results)
def print_summary(self):
"""Print a comprehensive market summary."""
utc_hour = datetime.now(timezone.utc).hour
active_sessions = self.get_active_sessions(utc_hour)
print("\n" + "=" * 70)
print("FOREX MARKET ANALYZER SUMMARY")
print("=" * 70)
print(f"\nAccount Balance: ${self.account_balance:,.2f}")
print(f"Current UTC Time: {utc_hour:02d}:00")
print(f"Active Sessions: {', '.join(active_sessions) if active_sessions else 'None'}")
if self.watched_pairs:
print("\n" + "-" * 70)
print("WATCHED PAIRS")
print("-" * 70)
overview = self.get_market_overview(utc_hour)
print(overview.to_string(index=False))
print("\n" + "=" * 70)
# Demonstrate the Forex Market Analyzer
# Create analyzer with $25,000 account
analyzer = ForexMarketAnalyzer(account_balance=25000)
# Add pairs to watchlist
pairs_to_watch = [
('EUR/USD', 1.0848, 1.0850),
('GBP/USD', 1.2648, 1.2652),
('USD/JPY', 150.48, 150.51),
('AUD/USD', 0.6548, 0.6551),
('EUR/GBP', 0.8578, 0.8582),
('USD/CAD', 1.3648, 1.3652)
]
for pair, bid, ask in pairs_to_watch:
analyzer.add_pair(pair, bid, ask)
# Print summary
analyzer.print_summary()
# Example: Position sizing calculation
print("Position Sizing Examples")
print("=" * 50)
# Calculate position size for 1% risk with 50 pip stop
for pair in ['EUR/USD', 'GBP/USD', 'USD/JPY']:
sizing = analyzer.calculate_position_size(pair, stop_loss_pips=50, risk_percent=1.0)
print(f"\n{pair}:")
print(f" Risk: ${sizing['risk_amount']:.2f} (1% of ${sizing['account_balance']:,.0f})")
print(f" Stop Loss: {sizing['stop_loss_pips']} pips")
print(f" Position: {sizing['position_lots']:.2f} lots ({sizing['position_units']:,} units)")
# Example: Trade P&L calculation
print("\nTrade P&L Examples")
print("=" * 60)
# Simulate some trades
trades = [
('EUR/USD', 1.0850, 1.0900, 0.5, 'long'), # 50 pip win
('GBP/USD', 1.2700, 1.2650, 0.3, 'short'), # 50 pip win short
('USD/JPY', 150.00, 149.60, 0.2, 'short'), # 40 pip win
]
for pair, entry, exit_price, lots, direction in trades:
result = analyzer.calculate_pnl(pair, entry, exit_price, lots, direction)
sign = '+' if result['pnl'] > 0 else ''
print(f"{result['pair']} {result['direction'].upper():5} {result['lots']:.1f} lots: "
f"{result['entry']} → {result['exit']} = {sign}{result['pips']} pips (${sign}{result['pnl']:.2f})")
# Example: Session analysis
print("\nSession Analysis for Different Hours")
print("=" * 70)
test_hours = [3, 8, 14, 20]
pair = 'EUR/USD'
for hour in test_hours:
conditions = analyzer.analyze_trading_conditions(pair, hour)
print(f"\n{pair} at {hour:02d}:00 UTC:")
print(f" Active Sessions: {', '.join(conditions['active_sessions']) if conditions['active_sessions'] else 'None'}")
print(f" Relevant Sessions: {', '.join(conditions['relevant_sessions'])}")
print(f" Trading Rating: {conditions['trading_rating']}")
print(f" Expected Volatility: {conditions['expected_volatility']}")
Key Takeaways
- Currency pairs consist of a base currency and quote currency - buying EUR/USD means buying euros and selling dollars
- Major pairs include USD and offer the tightest spreads; minor pairs are crosses between major currencies; exotic pairs include emerging market currencies
- The forex market is decentralized (OTC) with trading flowing through a hierarchy from interbank to retail
- Four major trading sessions (Sydney, Tokyo, London, New York) provide 24/5 market access with session overlaps offering the highest volatility
- Pips are the standard price movement unit (0.0001 for most pairs, 0.01 for JPY pairs)
- Lot sizes standardize position sizes: standard (100k), mini (10k), micro (1k)
- Position sizing based on risk percentage and stop loss is critical for account management
Next: Module 2 - Futures Market Basics
Module 2: Futures Market Basics
Part 1: Market Fundamentals
| Duration | Exercises |
|---|---|
| ~2.5 hours | 6 |
Learning Objectives
By the end of this module, you will be able to:
- Understand futures contract specifications and mechanics
- Navigate popular futures markets (indices, commodities, currencies)
- Explain the relationship between spot and futures prices
- Understand contango, backwardation, and basis
- Handle contract expiration and rollover
- Build continuous futures price series
# Standard imports for this module
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from datetime import datetime, timedelta
from typing import Dict, List, Tuple, Optional
from dataclasses import dataclass
from enum import Enum
# Display settings
pd.set_option('display.max_columns', 15)
pd.set_option('display.width', 200)
plt.style.use('seaborn-v0_8-whitegrid')
2.1 What are Futures?
A futures contract is a standardized agreement to buy or sell an underlying asset at a predetermined price on a specific future date.
Key Characteristics
| Feature | Description |
|---|---|
| Standardized | Exchange-defined contract size, quality, delivery |
| Leverage | Only margin required, not full contract value |
| Expiration | Contracts expire on specific dates |
| Settlement | Physical delivery or cash settlement |
| Mark-to-Market | Daily profit/loss settlement |
Contract Specifications
Every futures contract has specific details defined by the exchange:
E-mini S&P 500 (ES) Contract Specifications:
├── Exchange: CME
├── Contract Size: $50 × S&P 500 Index
├── Tick Size: 0.25 index points
├── Tick Value: $12.50
├── Trading Hours: Nearly 24 hours (Sunday-Friday)
├── Expiration: March, June, September, December
└── Settlement: Cash settled
class SettlementType(Enum):
"""Types of futures settlement."""
CASH = "cash"
PHYSICAL = "physical"
@dataclass
class FuturesContract:
"""
Represents a futures contract specification.
Attributes:
symbol: Contract symbol (e.g., 'ES')
name: Full contract name
exchange: Trading exchange
contract_size: Size multiplier
tick_size: Minimum price movement
tick_value: Dollar value per tick
settlement: Settlement type
months: Trading months (letter codes)
"""
symbol: str
name: str
exchange: str
contract_size: float
tick_size: float
tick_value: float
settlement: SettlementType
months: List[str]
@property
def point_value(self) -> float:
"""Calculate the dollar value of one full point move."""
ticks_per_point = 1 / self.tick_size
return ticks_per_point * self.tick_value
def calculate_pnl(self, entry: float, exit: float, contracts: int = 1) -> float:
"""Calculate P&L for a trade."""
points = exit - entry
return points * self.point_value * contracts
def ticks_to_points(self, ticks: int) -> float:
"""Convert ticks to points."""
return ticks * self.tick_size
def points_to_ticks(self, points: float) -> int:
"""Convert points to ticks."""
return int(points / self.tick_size)
# Define popular futures contracts
FUTURES_CONTRACTS = {
'ES': FuturesContract(
symbol='ES',
name='E-mini S&P 500',
exchange='CME',
contract_size=50,
tick_size=0.25,
tick_value=12.50,
settlement=SettlementType.CASH,
months=['H', 'M', 'U', 'Z'] # Mar, Jun, Sep, Dec
),
'NQ': FuturesContract(
symbol='NQ',
name='E-mini Nasdaq 100',
exchange='CME',
contract_size=20,
tick_size=0.25,
tick_value=5.00,
settlement=SettlementType.CASH,
months=['H', 'M', 'U', 'Z']
),
'CL': FuturesContract(
symbol='CL',
name='Crude Oil',
exchange='NYMEX',
contract_size=1000, # barrels
tick_size=0.01,
tick_value=10.00,
settlement=SettlementType.PHYSICAL,
months=['F', 'G', 'H', 'J', 'K', 'M', 'N', 'Q', 'U', 'V', 'X', 'Z']
),
'GC': FuturesContract(
symbol='GC',
name='Gold',
exchange='COMEX',
contract_size=100, # troy ounces
tick_size=0.10,
tick_value=10.00,
settlement=SettlementType.PHYSICAL,
months=['G', 'J', 'M', 'Q', 'V', 'Z']
),
'6E': FuturesContract(
symbol='6E',
name='Euro FX',
exchange='CME',
contract_size=125000, # euros
tick_size=0.00005,
tick_value=6.25,
settlement=SettlementType.CASH,
months=['H', 'M', 'U', 'Z']
),
'ZB': FuturesContract(
symbol='ZB',
name='30-Year Treasury Bond',
exchange='CBOT',
contract_size=100000,
tick_size=1/32,
tick_value=31.25,
settlement=SettlementType.PHYSICAL,
months=['H', 'M', 'U', 'Z']
)
}
# Display contract specifications
print("Popular Futures Contract Specifications")
print("=" * 80)
for symbol, contract in FUTURES_CONTRACTS.items():
print(f"\n{contract.name} ({symbol})")
print(f" Exchange: {contract.exchange}")
print(f" Contract Size: {contract.contract_size:,}")
print(f" Tick Size: {contract.tick_size}")
print(f" Tick Value: ${contract.tick_value:.2f}")
print(f" Point Value: ${contract.point_value:.2f}")
print(f" Settlement: {contract.settlement.value}")
# Demonstrate P&L calculations
es = FUTURES_CONTRACTS['ES']
print("E-mini S&P 500 (ES) P&L Examples")
print("=" * 50)
# Example trades
trades = [
(5000.00, 5010.00, 1, 'Long'), # 10 point gain
(5000.00, 4990.00, 2, 'Long'), # 10 point loss x2
(5000.00, 4980.00, 1, 'Short'), # 20 point gain (short)
]
for entry, exit_price, contracts, direction in trades:
if direction == 'Short':
pnl = es.calculate_pnl(exit_price, entry, contracts) # Reversed for short
else:
pnl = es.calculate_pnl(entry, exit_price, contracts)
points = abs(exit_price - entry)
ticks = es.points_to_ticks(points)
sign = '+' if pnl > 0 else ''
print(f"{direction:5} {contracts} contract(s): {entry} → {exit_price} = "
f"{points} pts ({ticks} ticks) = {sign}${pnl:,.2f}")
2.2 Popular Futures Markets
Futures are traded on various underlying assets:
Market Categories
| Category | Examples | Key Contracts |
|---|---|---|
| Equity Indices | S&P 500, Nasdaq, Dow | ES, NQ, YM |
| Energy | Crude Oil, Natural Gas | CL, NG |
| Metals | Gold, Silver, Copper | GC, SI, HG |
| Currencies | Euro, Yen, Pound | 6E, 6J, 6B |
| Interest Rates | Treasury Bonds, Eurodollar | ZB, ZN, GE |
| Agriculture | Corn, Wheat, Soybeans | ZC, ZW, ZS |
# Futures market categories
FUTURES_CATEGORIES = {
'Equity Indices': {
'description': 'Stock market index futures',
'contracts': {
'ES': 'E-mini S&P 500',
'NQ': 'E-mini Nasdaq 100',
'YM': 'E-mini Dow Jones',
'RTY': 'E-mini Russell 2000',
'MES': 'Micro E-mini S&P 500'
},
'key_drivers': ['Economic data', 'Earnings', 'Fed policy', 'Geopolitics']
},
'Energy': {
'description': 'Oil, gas, and energy products',
'contracts': {
'CL': 'Crude Oil (WTI)',
'BZ': 'Brent Crude Oil',
'NG': 'Natural Gas',
'RB': 'RBOB Gasoline',
'HO': 'Heating Oil'
},
'key_drivers': ['OPEC decisions', 'Inventory data', 'Weather', 'Geopolitics']
},
'Metals': {
'description': 'Precious and industrial metals',
'contracts': {
'GC': 'Gold',
'SI': 'Silver',
'HG': 'Copper',
'PL': 'Platinum',
'PA': 'Palladium'
},
'key_drivers': ['USD strength', 'Inflation', 'Safe haven flows', 'Industrial demand']
},
'Currencies': {
'description': 'Currency futures (CME)',
'contracts': {
'6E': 'Euro FX',
'6J': 'Japanese Yen',
'6B': 'British Pound',
'6A': 'Australian Dollar',
'6C': 'Canadian Dollar'
},
'key_drivers': ['Interest rates', 'Central bank policy', 'Economic data']
},
'Interest Rates': {
'description': 'Treasury and interest rate futures',
'contracts': {
'ZB': '30-Year Treasury Bond',
'ZN': '10-Year Treasury Note',
'ZF': '5-Year Treasury Note',
'ZT': '2-Year Treasury Note',
'SR3': '3-Month SOFR'
},
'key_drivers': ['Fed policy', 'Inflation expectations', 'Economic growth']
},
'Agriculture': {
'description': 'Grains, softs, and livestock',
'contracts': {
'ZC': 'Corn',
'ZW': 'Wheat',
'ZS': 'Soybeans',
'KC': 'Coffee',
'LE': 'Live Cattle'
},
'key_drivers': ['Weather', 'USDA reports', 'Global demand', 'Planting/harvest']
}
}
# Display market categories
print("Futures Market Categories")
print("=" * 70)
for category, info in FUTURES_CATEGORIES.items():
print(f"\n{category}")
print(f" {info['description']}")
print(f" Contracts: {', '.join(info['contracts'].keys())}")
print(f" Key Drivers: {', '.join(info['key_drivers'][:3])}")
# Compare contract sizes and margins
def get_notional_value(contract: FuturesContract, price: float) -> float:
"""Calculate the notional value of a futures contract."""
if contract.symbol in ['ES', 'NQ', 'YM']: # Index futures
return contract.contract_size * price
elif contract.symbol == 'CL': # Crude oil
return contract.contract_size * price # 1000 barrels * price
elif contract.symbol == 'GC': # Gold
return contract.contract_size * price # 100 oz * price
elif contract.symbol == '6E': # Euro FX
return contract.contract_size * price # 125000 euros * rate
else:
return contract.contract_size * price
# Sample prices for calculations
sample_prices = {
'ES': 5000,
'NQ': 17500,
'CL': 75.00,
'GC': 2050,
'6E': 1.0850
}
# Approximate initial margins (varies by broker)
initial_margins = {
'ES': 12000,
'NQ': 16000,
'CL': 6000,
'GC': 8000,
'6E': 2500
}
print("Futures Contract Size Comparison")
print("=" * 70)
print(f"{'Contract':<12} {'Price':<12} {'Notional':<15} {'Margin':<12} {'Leverage'}")
print("-" * 70)
for symbol in ['ES', 'NQ', 'CL', 'GC', '6E']:
contract = FUTURES_CONTRACTS[symbol]
price = sample_prices[symbol]
notional = get_notional_value(contract, price)
margin = initial_margins[symbol]
leverage = notional / margin
print(f"{symbol:<12} ${price:<11,.2f} ${notional:<14,.0f} ${margin:<11,} {leverage:.1f}:1")
Exercise 2.1: Read Contract Specifications (Guided)
Complete the function to parse and analyze futures contract specifications.
Solution 2.1
def analyze_contract(symbol: str, current_price: float) -> dict:
"""
Analyze a futures contract's specifications and calculate key metrics.
Args:
symbol: Contract symbol (e.g., 'ES')
current_price: Current contract price
Returns:
Dictionary with contract analysis
"""
# Get contract from our definitions
contract = FUTURES_CONTRACTS.get(symbol.upper()) # Get contract by key
if contract is None:
return {'error': f'Contract {symbol} not found'}
# Calculate notional value
notional = contract.contract_size * current_price # Multiply by price
# Calculate value of 1% move
one_percent_move = current_price * 0.01 # 1% = 0.01
one_percent_pnl = one_percent_move * contract.point_value
# Calculate ticks in a 1% move
ticks_in_one_percent = contract.points_to_ticks(one_percent_move)
return {
'symbol': symbol.upper(),
'name': contract.name,
'exchange': contract.exchange,
'current_price': current_price,
'notional_value': round(notional, 2),
'point_value': contract.point_value,
'tick_size': contract.tick_size,
'tick_value': contract.tick_value,
'one_percent_move_points': round(one_percent_move, 2),
'one_percent_move_ticks': ticks_in_one_percent,
'one_percent_pnl': round(one_percent_pnl, 2)
}
2.3 Futures vs Spot
Understanding Basis
The basis is the difference between the futures price and the spot price:
Basis = Futures Price - Spot Price
Contango vs Backwardation
| Condition | Basis | Futures vs Spot | Common In |
|---|---|---|---|
| Contango | Positive | Futures > Spot | Normal markets, storage costs |
| Backwardation | Negative | Futures < Spot | Supply shortages, high demand |
Cost of Carry Model
In theory, the futures price should reflect:
Futures Price = Spot Price × (1 + r - y)^t
Where:
- r = risk-free interest rate
- y = convenience yield (benefit of holding physical)
- t = time to expiration
@dataclass
class BasisAnalysis:
"""
Analysis of futures basis.
Attributes:
spot_price: Current spot price
futures_price: Futures contract price
days_to_expiry: Days until contract expiration
"""
spot_price: float
futures_price: float
days_to_expiry: int
@property
def basis(self) -> float:
"""Calculate absolute basis."""
return self.futures_price - self.spot_price
@property
def basis_percent(self) -> float:
"""Calculate basis as percentage of spot."""
return (self.basis / self.spot_price) * 100
@property
def annualized_basis(self) -> float:
"""Annualize the basis percentage."""
if self.days_to_expiry <= 0:
return 0
return self.basis_percent * (365 / self.days_to_expiry)
@property
def market_condition(self) -> str:
"""Determine market condition."""
if self.basis > 0:
return 'Contango'
elif self.basis < 0:
return 'Backwardation'
else:
return 'Flat'
def implied_yield(self) -> float:
"""Calculate implied yield/cost of carry."""
if self.days_to_expiry <= 0:
return 0
return self.annualized_basis
# Example basis analysis
examples = [
('Gold', 2050, 2065, 60), # Contango
('Crude Oil', 75.00, 73.50, 30), # Backwardation
('S&P 500', 5000, 5012, 45), # Slight contango
]
print("Basis Analysis Examples")
print("=" * 70)
for name, spot, futures, days in examples:
analysis = BasisAnalysis(spot, futures, days)
print(f"\n{name}:")
print(f" Spot: ${spot:,.2f} | Futures: ${futures:,.2f} | Days: {days}")
print(f" Basis: ${analysis.basis:,.2f} ({analysis.basis_percent:.2f}%)")
print(f" Annualized: {analysis.annualized_basis:.2f}%")
print(f" Market: {analysis.market_condition}")
# Visualize contango and backwardation
def plot_term_structure(contracts: List[Tuple[str, float, int]], title: str):
"""
Plot futures term structure.
Args:
contracts: List of (name, price, days_to_expiry) tuples
title: Chart title
"""
fig, ax = plt.subplots(figsize=(10, 5))
# Sort by expiry
contracts = sorted(contracts, key=lambda x: x[2])
names = [c[0] for c in contracts]
prices = [c[1] for c in contracts]
days = [c[2] for c in contracts]
# Plot
ax.plot(days, prices, 'bo-', linewidth=2, markersize=8)
# Add labels
for i, (name, price, day) in enumerate(contracts):
ax.annotate(f'{name}\n${price:.2f}', (day, price),
textcoords='offset points', xytext=(0, 10),
ha='center', fontsize=9)
# Determine structure
if prices[-1] > prices[0]:
structure = 'CONTANGO'
color = 'green'
else:
structure = 'BACKWARDATION'
color = 'red'
ax.text(0.95, 0.95, structure, transform=ax.transAxes,
fontsize=14, fontweight='bold', color=color,
ha='right', va='top')
ax.set_xlabel('Days to Expiration')
ax.set_ylabel('Price')
ax.set_title(title)
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
# Example: Gold in contango
gold_contracts = [
('Spot', 2050, 0),
('Feb', 2055, 30),
('Apr', 2062, 90),
('Jun', 2070, 150),
('Aug', 2078, 210),
]
plot_term_structure(gold_contracts, 'Gold Futures Term Structure')
# Example: Oil in backwardation
oil_contracts = [
('Spot', 78.00, 0),
('Feb', 76.50, 30),
('Mar', 75.20, 60),
('Apr', 74.10, 90),
('May', 73.20, 120),
]
plot_term_structure(oil_contracts, 'Crude Oil Futures Term Structure')
Exercise 2.2: Analyze Basis (Guided)
Complete the function to calculate roll yield and analyze term structure.
Solution 2.2
def calculate_roll_yield(
front_month_price: float,
back_month_price: float,
days_between: int
) -> dict:
"""
Calculate the roll yield when rolling from front to back month.
Args:
front_month_price: Price of expiring contract
back_month_price: Price of next contract
days_between: Days between contract expirations
Returns:
Dictionary with roll yield analysis
"""
# Calculate raw roll
roll_difference = back_month_price - front_month_price
# Calculate roll yield percentage
roll_yield_pct = (roll_difference / front_month_price) * 100 # Convert to percentage
# Annualize the roll yield
annualized_yield = roll_yield_pct * (365 / days_between) # Days in year
# Determine if positive or negative roll
if roll_difference > 0:
roll_type = 'Negative Roll Yield' # Contango hurts long holders
structure = 'Contango' # Market structure name
else:
roll_type = 'Positive Roll Yield' # Backwardation helps long holders
structure = 'Backwardation'
return {
'front_price': front_month_price,
'back_price': back_month_price,
'roll_difference': round(roll_difference, 4),
'roll_yield_pct': round(roll_yield_pct, 2),
'annualized_yield': round(annualized_yield, 2),
'roll_type': roll_type,
'structure': structure
}
2.4 Contract Months & Rollover
Contract Month Codes
Futures contracts use single-letter codes for expiration months:
| Code | Month | Code | Month |
|---|---|---|---|
| F | January | N | July |
| G | February | Q | August |
| H | March | U | September |
| J | April | V | October |
| K | May | X | November |
| M | June | Z | December |
Contract Symbol Format
ESH24 = E-mini S&P 500, March 2024
│└┴── Year (24 = 2024)
└──── Month (H = March)
# Contract month codes
MONTH_CODES = {
'F': (1, 'January'),
'G': (2, 'February'),
'H': (3, 'March'),
'J': (4, 'April'),
'K': (5, 'May'),
'M': (6, 'June'),
'N': (7, 'July'),
'Q': (8, 'August'),
'U': (9, 'September'),
'V': (10, 'October'),
'X': (11, 'November'),
'Z': (12, 'December')
}
# Reverse lookup
MONTH_TO_CODE = {v[0]: k for k, v in MONTH_CODES.items()}
class FuturesSymbolParser:
"""
Parser for futures contract symbols.
Handles symbols like ESH24, CLZ25, etc.
"""
def __init__(self, symbol: str):
self.raw_symbol = symbol.upper()
self._parse()
def _parse(self):
"""Parse the symbol into components."""
# Assume format: ROOT + MONTH_CODE + YEAR (2 digits)
# e.g., ESH24, CLZ25, GCM24
# Find the month code (second to last character before year)
if len(self.raw_symbol) >= 4:
self.year_str = self.raw_symbol[-2:]
self.month_code = self.raw_symbol[-3]
self.root = self.raw_symbol[:-3]
else:
raise ValueError(f"Invalid symbol format: {self.raw_symbol}")
@property
def year(self) -> int:
"""Get full year."""
year_2digit = int(self.year_str)
return 2000 + year_2digit if year_2digit < 50 else 1900 + year_2digit
@property
def month(self) -> int:
"""Get month number."""
return MONTH_CODES[self.month_code][0]
@property
def month_name(self) -> str:
"""Get month name."""
return MONTH_CODES[self.month_code][1]
@property
def expiration_date(self) -> datetime:
"""Estimate expiration date (third Friday of month)."""
# Find third Friday
first_day = datetime(self.year, self.month, 1)
# Days until Friday (4 = Friday)
days_until_friday = (4 - first_day.weekday()) % 7
first_friday = first_day + timedelta(days=days_until_friday)
third_friday = first_friday + timedelta(weeks=2)
return third_friday
def days_to_expiry(self, as_of: datetime = None) -> int:
"""Calculate days until expiration."""
if as_of is None:
as_of = datetime.now()
delta = self.expiration_date - as_of
return max(0, delta.days)
def __repr__(self) -> str:
return f"{self.root} {self.month_name} {self.year}"
# Test the parser
symbols = ['ESH25', 'CLZ25', 'GCM25', 'NQU25', '6EH25']
print("Futures Symbol Parsing")
print("=" * 60)
for sym in symbols:
parser = FuturesSymbolParser(sym)
print(f"{sym:8} -> {parser!r:25} (Expires: {parser.expiration_date.strftime('%Y-%m-%d')})")
class ContractRollover:
"""
Handles futures contract rollover logic.
Attributes:
root_symbol: Base contract symbol (e.g., 'ES')
contract_months: List of valid contract month codes
"""
def __init__(self, root_symbol: str, contract_months: List[str]):
self.root_symbol = root_symbol.upper()
self.contract_months = [m.upper() for m in contract_months]
def get_contract_symbol(self, month_code: str, year: int) -> str:
"""Generate full contract symbol."""
year_str = str(year)[-2:]
return f"{self.root_symbol}{month_code}{year_str}"
def get_active_contract(self, as_of: datetime = None) -> str:
"""
Get the currently active (front month) contract.
Args:
as_of: Reference date (default: today)
Returns:
Active contract symbol
"""
if as_of is None:
as_of = datetime.now()
current_month = as_of.month
current_year = as_of.year
# Find the next valid contract month
for month_code in self.contract_months:
month_num = MONTH_CODES[month_code][0]
if month_num > current_month:
return self.get_contract_symbol(month_code, current_year)
elif month_num == current_month:
# Check if we're past typical roll date (e.g., 2nd Thursday)
if as_of.day > 15: # Simplified: roll after 15th
continue
return self.get_contract_symbol(month_code, current_year)
# Roll to next year's first contract
return self.get_contract_symbol(self.contract_months[0], current_year + 1)
def get_contract_chain(self, num_contracts: int = 4, as_of: datetime = None) -> List[str]:
"""
Get a chain of upcoming contracts.
Args:
num_contracts: Number of contracts to return
as_of: Reference date
Returns:
List of contract symbols
"""
if as_of is None:
as_of = datetime.now()
contracts = []
current_month = as_of.month
current_year = as_of.year
# Build list of all possible contracts for next 2 years
all_contracts = []
for year in [current_year, current_year + 1]:
for month_code in self.contract_months:
month_num = MONTH_CODES[month_code][0]
if year == current_year and month_num < current_month:
continue
all_contracts.append(self.get_contract_symbol(month_code, year))
return all_contracts[:num_contracts]
# Test rollover logic
es_rollover = ContractRollover('ES', ['H', 'M', 'U', 'Z'])
print("E-mini S&P 500 Contract Chain")
print("=" * 40)
print(f"Active Contract: {es_rollover.get_active_contract()}")
print(f"\nUpcoming Contracts:")
for contract in es_rollover.get_contract_chain(4):
parser = FuturesSymbolParser(contract)
print(f" {contract} -> {parser.month_name} {parser.year}")
Exercise 2.3: Handle Rollover (Guided)
Complete the function to build a continuous futures price series by handling rollovers.
Solution 2.3
def build_continuous_series(
contract_data: Dict[str, pd.DataFrame],
roll_dates: Dict[str, datetime],
adjustment_method: str = 'ratio'
) -> pd.DataFrame:
"""
Build a continuous futures price series from individual contracts.
Args:
contract_data: Dict of contract symbol -> price DataFrame
roll_dates: Dict of contract symbol -> roll date
adjustment_method: 'ratio' or 'difference'
Returns:
Continuous price series
"""
# Sort contracts by roll date
sorted_contracts = sorted(roll_dates.items(), key=lambda x: x[1])
continuous = pd.DataFrame()
adjustment_factor = 1.0 if adjustment_method == 'ratio' else 0.0
for i, (contract, roll_date) in enumerate(sorted_contracts):
if contract not in contract_data:
continue
df = contract_data[contract].copy()
# Get data up to roll date
if i < len(sorted_contracts) - 1:
next_contract = sorted_contracts[i + 1][0]
next_roll = sorted_contracts[i + 1][1]
mask = df.index < roll_date # Filter by index (date)
df = df[mask]
# Calculate adjustment at roll point
if next_contract in contract_data:
next_df = contract_data[next_contract]
# Find prices at roll date
try:
old_price = df['close'].iloc[-1]
# Get first price from next contract after roll
next_prices = next_df[next_df.index >= roll_date]
if len(next_prices) > 0:
new_price = next_prices['close'].iloc[0]
if adjustment_method == 'ratio':
adjustment_factor *= new_price / old_price # Divide by old price
else:
adjustment_factor += new_price - old_price
except:
pass
# Apply adjustment
if adjustment_method == 'ratio': # Check method type
df['adjusted_close'] = df['close'] * adjustment_factor
else:
df['adjusted_close'] = df['close'] + adjustment_factor
continuous = pd.concat([continuous, df[['close', 'adjusted_close']]])
return continuous.sort_index()
Exercise 2.4: Futures Contract Analyzer (Open-ended)
Build a comprehensive futures contract analyzer that calculates margin requirements, leverage, and risk metrics.
Your implementation:
Solution 2.4
class FuturesAnalyzer:
"""
Comprehensive analyzer for futures contracts.
Attributes:
contract: FuturesContract specification
current_price: Current contract price
"""
def __init__(self, symbol: str, current_price: float):
self.contract = FUTURES_CONTRACTS.get(symbol.upper())
if self.contract is None:
raise ValueError(f"Unknown contract: {symbol}")
self.current_price = current_price
def get_notional_value(self) -> float:
"""Calculate the notional value of one contract."""
return self.contract.contract_size * self.current_price
def get_leverage(self, margin: float) -> float:
"""Calculate leverage based on margin requirement."""
return self.get_notional_value() / margin
def position_size_by_risk(
self,
account: float,
risk_pct: float,
stop_ticks: int
) -> int:
"""Calculate position size based on risk parameters."""
risk_amount = account * (risk_pct / 100)
risk_per_contract = stop_ticks * self.contract.tick_value
return max(1, int(risk_amount / risk_per_contract))
def scenario_analysis(self, price_changes: List[float]) -> pd.DataFrame:
"""Analyze P&L under different price scenarios."""
results = []
for change_pct in price_changes:
new_price = self.current_price * (1 + change_pct / 100)
points_change = new_price - self.current_price
pnl = points_change * self.contract.point_value
results.append({
'Price Change (%)': f"{change_pct:+.1f}%",
'New Price': round(new_price, 2),
'Points': round(points_change, 2),
'P&L (1 contract)': f"${pnl:+,.2f}"
})
return pd.DataFrame(results)
def summary(self) -> dict:
"""Get comprehensive contract summary."""
return {
'symbol': self.contract.symbol,
'name': self.contract.name,
'current_price': self.current_price,
'notional_value': f"${self.get_notional_value():,.2f}",
'point_value': f"${self.contract.point_value:.2f}",
'tick_size': self.contract.tick_size,
'tick_value': f"${self.contract.tick_value:.2f}"
}
# Test the analyzer
analyzer = FuturesAnalyzer('ES', 5000)
print("E-mini S&P 500 Analysis")
print("=" * 50)
for k, v in analyzer.summary().items():
print(f"{k}: {v}")
print(f"\nLeverage at $12,000 margin: {analyzer.get_leverage(12000):.1f}:1")
print(f"Position size ($50k account, 2% risk, 20 tick stop): "
f"{analyzer.position_size_by_risk(50000, 2, 20)} contracts")
print("\nScenario Analysis:")
print(analyzer.scenario_analysis([-5, -2, -1, 0, 1, 2, 5]).to_string(index=False))
Exercise 2.5: Term Structure Analyzer (Open-ended)
Build a term structure analyzer that tracks the futures curve and identifies trading opportunities.
Your implementation:
Solution 2.5
class TermStructureAnalyzer:
"""
Analyzes futures term structure for trading opportunities.
Attributes:
underlying: Name of the underlying asset
contracts: List of contract data
"""
def __init__(self, underlying: str):
self.underlying = underlying
self.contracts = []
def add_contract(self, symbol: str, price: float, expiry: datetime):
"""Add a contract to the term structure."""
days_to_expiry = (expiry - datetime.now()).days
self.contracts.append({
'symbol': symbol,
'price': price,
'expiry': expiry,
'days_to_expiry': max(0, days_to_expiry)
})
# Sort by expiry
self.contracts.sort(key=lambda x: x['expiry'])
def get_structure(self) -> str:
"""Determine overall term structure."""
if len(self.contracts) < 2:
return 'Unknown'
slopes = []
for i in range(len(self.contracts) - 1):
slope = self.contracts[i+1]['price'] - self.contracts[i]['price']
slopes.append(slope)
positive = sum(1 for s in slopes if s > 0)
negative = sum(1 for s in slopes if s < 0)
if positive == len(slopes):
return 'Contango'
elif negative == len(slopes):
return 'Backwardation'
else:
return 'Mixed'
def calculate_slope(self) -> float:
"""Calculate annualized term structure slope."""
if len(self.contracts) < 2:
return 0.0
front = self.contracts[0]
back = self.contracts[-1]
price_diff = back['price'] - front['price']
days_diff = back['days_to_expiry'] - front['days_to_expiry']
if days_diff <= 0:
return 0.0
# Annualized percentage
return (price_diff / front['price']) * (365 / days_diff) * 100
def find_spread_opportunities(self) -> List[dict]:
"""Find calendar spread opportunities."""
opportunities = []
for i in range(len(self.contracts) - 1):
front = self.contracts[i]
back = self.contracts[i + 1]
spread = back['price'] - front['price']
days_diff = back['days_to_expiry'] - front['days_to_expiry']
if days_diff > 0:
annualized = (spread / front['price']) * (365 / days_diff) * 100
else:
annualized = 0
opportunities.append({
'front': front['symbol'],
'back': back['symbol'],
'spread': round(spread, 4),
'spread_pct': round(spread / front['price'] * 100, 2),
'annualized_pct': round(annualized, 2),
'days_between': days_diff
})
return opportunities
def plot_term_structure(self):
"""Visualize the term structure."""
if not self.contracts:
print("No contracts to plot")
return
fig, ax = plt.subplots(figsize=(10, 5))
days = [c['days_to_expiry'] for c in self.contracts]
prices = [c['price'] for c in self.contracts]
symbols = [c['symbol'] for c in self.contracts]
ax.plot(days, prices, 'bo-', linewidth=2, markersize=10)
for i, (d, p, s) in enumerate(zip(days, prices, symbols)):
ax.annotate(f'{s}\n${p:.2f}', (d, p),
textcoords='offset points', xytext=(0, 12),
ha='center', fontsize=9)
structure = self.get_structure()
color = 'green' if structure == 'Contango' else 'red' if structure == 'Backwardation' else 'orange'
ax.text(0.95, 0.95, structure, transform=ax.transAxes,
fontsize=14, fontweight='bold', color=color,
ha='right', va='top')
ax.set_xlabel('Days to Expiration')
ax.set_ylabel('Price')
ax.set_title(f'{self.underlying} Futures Term Structure')
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
# Test the analyzer
analyzer = TermStructureAnalyzer('Crude Oil')
# Add contracts (backwardation example)
analyzer.add_contract('CLG25', 78.50, datetime(2025, 2, 20))
analyzer.add_contract('CLH25', 77.20, datetime(2025, 3, 20))
analyzer.add_contract('CLJ25', 76.10, datetime(2025, 4, 22))
analyzer.add_contract('CLK25', 75.20, datetime(2025, 5, 20))
print(f"Structure: {analyzer.get_structure()}")
print(f"Annualized Slope: {analyzer.calculate_slope():.2f}%")
print("\nSpread Opportunities:")
for opp in analyzer.find_spread_opportunities():
print(f" {opp['front']}/{opp['back']}: ${opp['spread']:.2f} "
f"({opp['annualized_pct']:.1f}% ann.)")
analyzer.plot_term_structure()
Exercise 2.6: Futures Data Handler (Open-ended)
Build a comprehensive data handler that manages multiple futures contracts and their data.
Your implementation:
Solution 2.6
class FuturesDataHandler:
"""
Comprehensive handler for futures contract data.
Manages multiple contracts, handles rollovers, and provides
analysis capabilities for futures data.
"""
def __init__(self, root_symbol: str, contract_months: List[str]):
self.root_symbol = root_symbol
self.contract_months = contract_months
self.contract_data = {}
self.roll_dates = {}
def add_contract_data(self, symbol: str, data: pd.DataFrame):
"""Add price data for a contract."""
symbol = symbol.upper()
self.contract_data[symbol] = data.copy()
# Parse expiration and set roll date
parser = FuturesSymbolParser(symbol)
# Roll 5 days before expiration
roll_date = parser.expiration_date - timedelta(days=5)
self.roll_dates[symbol] = roll_date
def get_front_month(self, as_of: datetime = None) -> str:
"""Get the front month contract symbol."""
if as_of is None:
as_of = datetime.now()
# Find contracts that haven't expired yet
active = []
for symbol, roll_date in self.roll_dates.items():
if roll_date > as_of:
parser = FuturesSymbolParser(symbol)
active.append((symbol, parser.expiration_date))
if not active:
return None
# Return the nearest expiration
active.sort(key=lambda x: x[1])
return active[0][0]
def build_continuous(self, method: str = 'ratio') -> pd.DataFrame:
"""Build continuous price series."""
return build_continuous_series(
self.contract_data,
self.roll_dates,
method
)
def get_term_structure(self, as_of: datetime = None) -> pd.DataFrame:
"""Get term structure as of a date."""
if as_of is None:
as_of = datetime.now()
results = []
for symbol, data in self.contract_data.items():
parser = FuturesSymbolParser(symbol)
# Get price as of date
if isinstance(data.index, pd.DatetimeIndex):
mask = data.index <= as_of
if mask.any():
price = data.loc[mask, 'close'].iloc[-1]
else:
continue
else:
price = data['close'].iloc[-1]
days_to_expiry = (parser.expiration_date - as_of).days
results.append({
'symbol': symbol,
'month': parser.month_name,
'year': parser.year,
'price': price,
'days_to_expiry': days_to_expiry
})
df = pd.DataFrame(results)
return df.sort_values('days_to_expiry')
def calculate_historical_basis(
self,
spot_data: pd.Series
) -> pd.DataFrame:
"""Calculate historical basis vs spot."""
continuous = self.build_continuous('ratio')
# Align dates
combined = pd.DataFrame({
'futures': continuous['adjusted_close'],
'spot': spot_data
}).dropna()
combined['basis'] = combined['futures'] - combined['spot']
combined['basis_pct'] = (combined['basis'] / combined['spot']) * 100
return combined
def summary(self) -> dict:
"""Get handler summary."""
return {
'root_symbol': self.root_symbol,
'contracts_loaded': len(self.contract_data),
'contract_symbols': list(self.contract_data.keys()),
'front_month': self.get_front_month()
}
# Test the handler
handler = FuturesDataHandler('ES', ['H', 'M', 'U', 'Z'])
# Add mock data
for symbol, df in contract_data.items():
handler.add_contract_data(symbol, df)
print("Futures Data Handler Summary")
print("=" * 40)
for k, v in handler.summary().items():
print(f"{k}: {v}")
print("\nTerm Structure:")
print(handler.get_term_structure(datetime(2024, 2, 1)))
Module Project: Futures Data Handler
Build a comprehensive futures data handler that combines all concepts from this module.
class ComprehensiveFuturesHandler:
"""
Complete futures data and analysis system.
Combines contract specifications, price data management,
rollover handling, and term structure analysis.
"""
def __init__(self, root_symbol: str):
"""
Initialize the handler.
Args:
root_symbol: Base symbol (e.g., 'ES', 'CL')
"""
self.root_symbol = root_symbol.upper()
self.contract_spec = FUTURES_CONTRACTS.get(self.root_symbol)
self.contracts = {} # symbol -> {'data': df, 'expiry': datetime}
self.spot_data = None
self.continuous_data = None
# ==================== Contract Management ====================
def add_contract(
self,
symbol: str,
data: pd.DataFrame,
expiry: datetime = None
):
"""
Add a contract with its price data.
Args:
symbol: Full contract symbol (e.g., 'ESH25')
data: OHLCV DataFrame
expiry: Expiration date (auto-calculated if None)
"""
symbol = symbol.upper()
if expiry is None:
parser = FuturesSymbolParser(symbol)
expiry = parser.expiration_date
self.contracts[symbol] = {
'data': data.copy(),
'expiry': expiry,
'roll_date': expiry - timedelta(days=5)
}
def set_spot_data(self, data: pd.Series):
"""Set spot price data for basis calculations."""
self.spot_data = data.copy()
def get_active_contracts(self, as_of: datetime = None) -> List[str]:
"""Get list of active (non-expired) contracts."""
if as_of is None:
as_of = datetime.now()
active = [
symbol for symbol, info in self.contracts.items()
if info['expiry'] > as_of
]
# Sort by expiry
active.sort(key=lambda s: self.contracts[s]['expiry'])
return active
def get_front_month(self, as_of: datetime = None) -> Optional[str]:
"""Get the front month contract."""
active = self.get_active_contracts(as_of)
return active[0] if active else None
# ==================== Continuous Contract ====================
def build_continuous(
self,
method: str = 'ratio',
roll_days_before: int = 5
) -> pd.DataFrame:
"""
Build continuous price series.
Args:
method: Adjustment method ('ratio' or 'difference')
roll_days_before: Days before expiry to roll
Returns:
Continuous price DataFrame
"""
if not self.contracts:
return pd.DataFrame()
# Sort contracts by expiry
sorted_contracts = sorted(
self.contracts.items(),
key=lambda x: x[1]['expiry']
)
continuous = pd.DataFrame()
adjustment = 1.0 if method == 'ratio' else 0.0
for i, (symbol, info) in enumerate(sorted_contracts):
df = info['data'].copy()
roll_date = info['roll_date']
# Filter data up to roll date
if i < len(sorted_contracts) - 1:
df = df[df.index < roll_date]
# Calculate adjustment
if len(df) > 0:
next_symbol = sorted_contracts[i + 1][0]
next_data = self.contracts[next_symbol]['data']
try:
old_price = df['close'].iloc[-1]
new_data = next_data[next_data.index >= roll_date]
if len(new_data) > 0:
new_price = new_data['close'].iloc[0]
if method == 'ratio':
adjustment *= new_price / old_price
else:
adjustment += new_price - old_price
except Exception:
pass
# Apply adjustment
df['contract'] = symbol
if method == 'ratio':
df['adjusted'] = df['close'] * adjustment
else:
df['adjusted'] = df['close'] + adjustment
continuous = pd.concat([continuous, df])
self.continuous_data = continuous.sort_index()
return self.continuous_data
# ==================== Term Structure ====================
def get_term_structure(self, as_of: datetime = None) -> pd.DataFrame:
"""
Get term structure snapshot.
Args:
as_of: Reference date
Returns:
DataFrame with term structure
"""
if as_of is None:
as_of = datetime.now()
results = []
for symbol, info in self.contracts.items():
data = info['data']
expiry = info['expiry']
# Get price as of date
mask = data.index <= as_of
if not mask.any():
continue
price = data.loc[mask, 'close'].iloc[-1]
days_to_expiry = (expiry - as_of).days
results.append({
'symbol': symbol,
'price': price,
'expiry': expiry.strftime('%Y-%m-%d'),
'days_to_expiry': days_to_expiry
})
df = pd.DataFrame(results)
if not df.empty:
df = df.sort_values('days_to_expiry')
return df
def analyze_term_structure(self, as_of: datetime = None) -> dict:
"""
Analyze current term structure.
Returns:
Analysis dictionary
"""
ts = self.get_term_structure(as_of)
if len(ts) < 2:
return {'structure': 'Unknown', 'error': 'Insufficient data'}
# Calculate price changes
prices = ts['price'].values
changes = np.diff(prices)
# Determine structure
if all(c > 0 for c in changes):
structure = 'Contango'
elif all(c < 0 for c in changes):
structure = 'Backwardation'
else:
structure = 'Mixed'
# Calculate front-to-back spread
front_price = ts.iloc[0]['price']
back_price = ts.iloc[-1]['price']
total_spread = back_price - front_price
spread_pct = (total_spread / front_price) * 100
# Annualize
days_span = ts.iloc[-1]['days_to_expiry'] - ts.iloc[0]['days_to_expiry']
if days_span > 0:
annualized = spread_pct * (365 / days_span)
else:
annualized = 0
return {
'structure': structure,
'front_month': ts.iloc[0]['symbol'],
'back_month': ts.iloc[-1]['symbol'],
'total_spread': round(total_spread, 4),
'spread_pct': round(spread_pct, 2),
'annualized_pct': round(annualized, 2),
'num_contracts': len(ts)
}
# ==================== Basis Analysis ====================
def calculate_basis(self, as_of: datetime = None) -> Optional[dict]:
"""
Calculate current basis vs spot.
Returns:
Basis analysis dictionary
"""
if self.spot_data is None:
return None
if as_of is None:
as_of = datetime.now()
# Get spot price
spot_mask = self.spot_data.index <= as_of
if not spot_mask.any():
return None
spot_price = self.spot_data[spot_mask].iloc[-1]
# Get front month futures price
front = self.get_front_month(as_of)
if front is None:
return None
futures_data = self.contracts[front]['data']
futures_mask = futures_data.index <= as_of
if not futures_mask.any():
return None
futures_price = futures_data.loc[futures_mask, 'close'].iloc[-1]
# Calculate basis
basis = futures_price - spot_price
basis_pct = (basis / spot_price) * 100
# Days to expiry
days_to_expiry = (self.contracts[front]['expiry'] - as_of).days
# Annualize
if days_to_expiry > 0:
annualized = basis_pct * (365 / days_to_expiry)
else:
annualized = 0
return {
'spot_price': round(spot_price, 4),
'futures_price': round(futures_price, 4),
'front_month': front,
'basis': round(basis, 4),
'basis_pct': round(basis_pct, 2),
'annualized_pct': round(annualized, 2),
'days_to_expiry': days_to_expiry,
'condition': 'Contango' if basis > 0 else 'Backwardation' if basis < 0 else 'Flat'
}
# ==================== Visualization ====================
def plot_term_structure(self, as_of: datetime = None):
"""Plot the current term structure."""
ts = self.get_term_structure(as_of)
if ts.empty:
print("No data to plot")
return
analysis = self.analyze_term_structure(as_of)
fig, ax = plt.subplots(figsize=(10, 5))
ax.plot(ts['days_to_expiry'], ts['price'], 'bo-', linewidth=2, markersize=10)
for _, row in ts.iterrows():
ax.annotate(
f"{row['symbol']}\n${row['price']:.2f}",
(row['days_to_expiry'], row['price']),
textcoords='offset points',
xytext=(0, 12),
ha='center',
fontsize=9
)
# Add structure label
color = 'green' if analysis['structure'] == 'Contango' else 'red'
ax.text(
0.95, 0.95,
f"{analysis['structure']}\n{analysis['annualized_pct']:.1f}% ann.",
transform=ax.transAxes,
fontsize=12,
fontweight='bold',
color=color,
ha='right',
va='top'
)
ax.set_xlabel('Days to Expiration')
ax.set_ylabel('Price')
ax.set_title(f'{self.root_symbol} Futures Term Structure')
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
def plot_continuous(self):
"""Plot continuous price series."""
if self.continuous_data is None:
self.build_continuous()
if self.continuous_data is None or self.continuous_data.empty:
print("No continuous data available")
return
fig, axes = plt.subplots(2, 1, figsize=(12, 8), height_ratios=[3, 1])
# Price chart
ax1 = axes[0]
ax1.plot(self.continuous_data.index, self.continuous_data['adjusted'],
label='Adjusted', linewidth=1.5)
ax1.plot(self.continuous_data.index, self.continuous_data['close'],
label='Unadjusted', linewidth=1, alpha=0.5)
# Mark roll dates
for symbol, info in self.contracts.items():
roll_date = info['roll_date']
if roll_date in self.continuous_data.index or \
any(self.continuous_data.index < roll_date):
ax1.axvline(roll_date, color='red', linestyle='--', alpha=0.5)
ax1.set_ylabel('Price')
ax1.set_title(f'{self.root_symbol} Continuous Futures')
ax1.legend()
ax1.grid(True, alpha=0.3)
# Contract chart
ax2 = axes[1]
contracts = self.continuous_data['contract'].unique()
colors = plt.cm.Set1(np.linspace(0, 1, len(contracts)))
for contract, color in zip(contracts, colors):
mask = self.continuous_data['contract'] == contract
data = self.continuous_data[mask]
ax2.fill_between(data.index, 0, 1, alpha=0.7, label=contract, color=color)
ax2.set_ylabel('Contract')
ax2.set_xlabel('Date')
ax2.legend(loc='upper left', ncol=len(contracts))
ax2.set_yticks([])
plt.tight_layout()
plt.show()
# ==================== Summary ====================
def summary(self) -> dict:
"""Get comprehensive handler summary."""
return {
'root_symbol': self.root_symbol,
'contract_spec': self.contract_spec.name if self.contract_spec else 'Unknown',
'contracts_loaded': len(self.contracts),
'contract_symbols': list(self.contracts.keys()),
'front_month': self.get_front_month(),
'has_spot_data': self.spot_data is not None,
'has_continuous': self.continuous_data is not None
}
def print_summary(self):
"""Print detailed summary."""
print("\n" + "=" * 60)
print(f"FUTURES DATA HANDLER: {self.root_symbol}")
print("=" * 60)
summary = self.summary()
print(f"\nContract: {summary['contract_spec']}")
print(f"Contracts Loaded: {summary['contracts_loaded']}")
print(f"Front Month: {summary['front_month']}")
if self.contracts:
print("\n" + "-" * 60)
print("TERM STRUCTURE")
print("-" * 60)
ts = self.get_term_structure()
print(ts.to_string(index=False))
analysis = self.analyze_term_structure()
print(f"\nStructure: {analysis['structure']}")
print(f"Spread: {analysis['spread_pct']:.2f}% ({analysis['annualized_pct']:.2f}% ann.)")
print("\n" + "=" * 60)
# Demonstrate the Comprehensive Futures Handler
# Create handler for E-mini S&P 500
handler = ComprehensiveFuturesHandler('ES')
# Generate mock data for multiple contracts
np.random.seed(42)
base_price = 5000
contracts_to_add = [
('ESH25', datetime(2025, 3, 21), 0),
('ESM25', datetime(2025, 6, 20), 5), # Slight contango
('ESU25', datetime(2025, 9, 19), 10),
('ESZ25', datetime(2025, 12, 19), 15),
]
for symbol, expiry, premium in contracts_to_add:
# Generate 90 days of data
dates = pd.date_range(end=datetime.now(), periods=90, freq='D')
prices = base_price + premium + np.cumsum(np.random.randn(90) * 15)
data = pd.DataFrame({
'open': prices - np.random.uniform(2, 5, 90),
'high': prices + np.random.uniform(5, 15, 90),
'low': prices - np.random.uniform(5, 15, 90),
'close': prices,
'volume': np.random.randint(100000, 500000, 90)
}, index=dates)
handler.add_contract(symbol, data, expiry)
# Print summary
handler.print_summary()
# Visualize term structure
handler.plot_term_structure()
# Build and visualize continuous contract
continuous = handler.build_continuous('ratio')
print("Continuous Series Sample:")
print(continuous.tail(10)[['close', 'adjusted', 'contract']])
Key Takeaways
- Futures contracts are standardized agreements with specific tick sizes, values, and expiration dates
- Contract specifications define the multiplier, tick value, and point value that determine P&L
- Popular markets include equity indices (ES, NQ), energy (CL, NG), metals (GC, SI), and currencies (6E, 6J)
- Basis is the difference between futures and spot prices; positive = contango, negative = backwardation
- Term structure shows how prices vary across expiration dates and impacts roll yield
- Contract months use letter codes (H=March, M=June, U=September, Z=December for quarterlies)
- Rollover requires adjusting for price gaps when switching contracts (ratio or difference method)
- Continuous contracts enable backtesting but require careful handling of roll adjustments
Next: Module 3 - Leverage & Margin
Module 3: Leverage & Margin
Part 1: Market Fundamentals
| Duration | Exercises |
|---|---|
| ~2.5 hours | 6 |
Learning Objectives
By the end of this module, you will be able to:
- Understand how leverage works in forex and futures markets
- Calculate margin requirements and leverage ratios
- Implement proper position sizing with leverage
- Manage margin calls and liquidation risk
- Build risk management systems for leveraged trading
# Standard imports for this module
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from dataclasses import dataclass
from typing import Dict, List, Optional, Tuple
from enum import Enum
# Display settings
pd.set_option('display.max_columns', 15)
pd.set_option('display.width', 200)
plt.style.use('seaborn-v0_8-whitegrid')
3.1 Understanding Leverage
Leverage allows you to control a large position with a small amount of capital. It's a double-edged sword that amplifies both gains and losses.
Leverage Formula
Leverage Ratio = Position Value / Margin Required
Example: 50:1 leverage
- Position Value: $100,000
- Margin Required: $2,000 (2%)
- Leverage: 100,000 / 2,000 = 50:1
Leverage by Market
| Market | Typical Leverage | Margin % |
|---|---|---|
| Forex (Retail) | 50:1 to 500:1 | 0.2% - 2% |
| Forex (US Regulated) | 50:1 max | 2% |
| Futures | 10:1 to 20:1 | 5% - 10% |
| Stocks (US) | 2:1 to 4:1 | 25% - 50% |
| Crypto | 2:1 to 125:1 | 0.8% - 50% |
@dataclass
class LeverageCalculator:
"""
Calculator for leverage and margin.
Attributes:
leverage_ratio: The leverage ratio (e.g., 50 for 50:1)
"""
leverage_ratio: float
@property
def margin_percent(self) -> float:
"""Calculate margin percentage."""
return 100 / self.leverage_ratio
def margin_required(self, position_value: float) -> float:
"""Calculate required margin for a position."""
return position_value / self.leverage_ratio
def max_position(self, available_margin: float) -> float:
"""Calculate maximum position size given margin."""
return available_margin * self.leverage_ratio
def leveraged_return(
self,
price_change_pct: float,
account_pct_at_risk: float = 100
) -> float:
"""
Calculate leveraged return on account.
Args:
price_change_pct: Underlying price change percentage
account_pct_at_risk: Percentage of account used as margin
Returns:
Account return percentage
"""
position_return = price_change_pct * self.leverage_ratio
return position_return * (account_pct_at_risk / 100)
def price_move_to_wipeout(self) -> float:
"""Calculate adverse price move needed to lose all margin."""
return 100 / self.leverage_ratio
# Compare leverage levels
leverage_levels = [10, 25, 50, 100, 200]
print("Leverage Comparison")
print("=" * 70)
print(f"{'Leverage':<12} {'Margin %':<12} {'$100k Position':<18} {'Wipeout Move'}")
print("-" * 70)
for lev in leverage_levels:
calc = LeverageCalculator(lev)
margin = calc.margin_required(100_000)
wipeout = calc.price_move_to_wipeout()
print(f"{lev}:1{'':<8} {calc.margin_percent:.1f}%{'':<8} ${margin:,.0f}{'':<12} {wipeout:.1f}%")
# Visualize leverage impact
def plot_leverage_impact():
"""Visualize how leverage amplifies returns."""
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
# Price changes from -5% to +5%
price_changes = np.linspace(-5, 5, 100)
leverage_levels = [1, 10, 50, 100]
colors = ['green', 'blue', 'orange', 'red']
# Left plot: Returns at different leverage
ax1 = axes[0]
for lev, color in zip(leverage_levels, colors):
calc = LeverageCalculator(lev)
returns = [calc.leveraged_return(pc) for pc in price_changes]
ax1.plot(price_changes, returns, label=f'{lev}:1', color=color, linewidth=2)
ax1.axhline(y=0, color='black', linestyle='-', linewidth=0.5)
ax1.axvline(x=0, color='black', linestyle='-', linewidth=0.5)
ax1.axhline(y=-100, color='red', linestyle='--', linewidth=1, alpha=0.7, label='Margin Call')
ax1.set_xlabel('Price Change (%)')
ax1.set_ylabel('Account Return (%)')
ax1.set_title('Leverage Impact on Returns')
ax1.legend()
ax1.set_ylim(-150, 150)
ax1.grid(True, alpha=0.3)
# Right plot: Margin call threshold
ax2 = axes[1]
leverage_range = np.arange(1, 201)
wipeout_moves = [LeverageCalculator(l).price_move_to_wipeout() for l in leverage_range]
ax2.plot(leverage_range, wipeout_moves, color='red', linewidth=2)
ax2.fill_between(leverage_range, wipeout_moves, 0, alpha=0.2, color='red')
# Add annotations for common leverage levels
annotations = [(50, 2), (100, 1), (200, 0.5)]
for lev, move in annotations:
ax2.annotate(
f'{lev}:1 = {move}%',
xy=(lev, move),
xytext=(lev + 20, move + 3),
arrowprops=dict(arrowstyle='->', color='black'),
fontsize=10
)
ax2.set_xlabel('Leverage Ratio')
ax2.set_ylabel('Adverse Move to Lose All Margin (%)')
ax2.set_title('Margin Call Threshold by Leverage')
ax2.grid(True, alpha=0.3)
ax2.set_ylim(0, 15)
plt.tight_layout()
plt.show()
plot_leverage_impact()
3.2 Margin Requirements
Types of Margin
| Margin Type | Description | Typical Level |
|---|---|---|
| Initial Margin | Required to open a position | 2% - 10% |
| Maintenance Margin | Required to keep position open | 50% - 75% of initial |
| Variation Margin | Daily P&L settlement (futures) | Based on price move |
Margin Call Process
1. Account equity falls below maintenance margin
2. Broker issues margin call
3. Trader must:
- Deposit additional funds, OR
- Close positions to reduce exposure
4. If no action: Broker liquidates positions
class MarginAccount:
"""
Simulates a margin trading account.
Attributes:
balance: Initial cash balance
initial_margin_pct: Initial margin requirement
maintenance_margin_pct: Maintenance margin requirement
"""
def __init__(
self,
balance: float,
initial_margin_pct: float = 2.0,
maintenance_margin_pct: float = 1.0
):
self.initial_balance = balance
self.cash = balance
self.initial_margin_pct = initial_margin_pct
self.maintenance_margin_pct = maintenance_margin_pct
self.positions = [] # List of open positions
self.trade_history = []
@property
def leverage_ratio(self) -> float:
"""Calculate effective leverage ratio."""
return 100 / self.initial_margin_pct
@property
def total_position_value(self) -> float:
"""Calculate total value of all positions."""
return sum(p['current_value'] for p in self.positions)
@property
def unrealized_pnl(self) -> float:
"""Calculate unrealized P&L."""
return sum(p['unrealized_pnl'] for p in self.positions)
@property
def equity(self) -> float:
"""Calculate account equity."""
return self.cash + self.unrealized_pnl
@property
def used_margin(self) -> float:
"""Calculate margin used by positions."""
return sum(p['margin_used'] for p in self.positions)
@property
def free_margin(self) -> float:
"""Calculate available margin."""
return self.equity - self.used_margin
@property
def margin_level(self) -> float:
"""Calculate margin level percentage."""
if self.used_margin == 0:
return float('inf')
return (self.equity / self.used_margin) * 100
def open_position(
self,
symbol: str,
direction: str,
size: float,
entry_price: float
) -> dict:
"""
Open a new position.
Args:
symbol: Trading symbol
direction: 'long' or 'short'
size: Position size (units or lots)
entry_price: Entry price
Returns:
Position details or error
"""
position_value = size * entry_price
margin_required = position_value * (self.initial_margin_pct / 100)
# Check if enough margin
if margin_required > self.free_margin:
return {'error': 'Insufficient margin'}
position = {
'id': len(self.positions) + 1,
'symbol': symbol,
'direction': direction,
'size': size,
'entry_price': entry_price,
'current_price': entry_price,
'current_value': position_value,
'margin_used': margin_required,
'unrealized_pnl': 0
}
self.positions.append(position)
return position
def update_prices(self, prices: Dict[str, float]):
"""
Update positions with new prices.
Args:
prices: Dict of symbol -> current price
"""
for pos in self.positions:
if pos['symbol'] in prices:
new_price = prices[pos['symbol']]
pos['current_price'] = new_price
pos['current_value'] = pos['size'] * new_price
# Calculate P&L
price_change = new_price - pos['entry_price']
if pos['direction'] == 'short':
price_change = -price_change
pos['unrealized_pnl'] = price_change * pos['size']
def check_margin_call(self) -> dict:
"""
Check if account is in margin call.
Returns:
Margin call status
"""
maintenance_required = self.used_margin * (self.maintenance_margin_pct / self.initial_margin_pct)
if self.equity <= 0:
return {
'status': 'LIQUIDATION',
'message': 'Account equity depleted',
'equity': self.equity
}
elif self.equity < maintenance_required:
return {
'status': 'MARGIN_CALL',
'message': 'Below maintenance margin',
'equity': self.equity,
'required': maintenance_required,
'shortfall': maintenance_required - self.equity
}
else:
return {
'status': 'OK',
'margin_level': self.margin_level
}
def summary(self) -> dict:
"""Get account summary."""
return {
'initial_balance': self.initial_balance,
'equity': round(self.equity, 2),
'unrealized_pnl': round(self.unrealized_pnl, 2),
'used_margin': round(self.used_margin, 2),
'free_margin': round(self.free_margin, 2),
'margin_level': round(self.margin_level, 2) if self.margin_level != float('inf') else 'N/A',
'leverage': f"{self.leverage_ratio:.0f}:1",
'positions': len(self.positions)
}
# Demonstrate margin account
account = MarginAccount(balance=10000, initial_margin_pct=2.0, maintenance_margin_pct=1.0)
print("Initial Account State")
print("=" * 40)
for k, v in account.summary().items():
print(f"{k}: {v}")
# Open a position and track margin
position = account.open_position('EUR/USD', 'long', 100000, 1.0850)
print("\nAfter Opening Position")
print("=" * 40)
print(f"Position: 100,000 EUR/USD at 1.0850")
print(f"Position Value: ${100000 * 1.0850:,.2f}")
print(f"Margin Used: ${position['margin_used']:,.2f}")
print("\nAccount:")
for k, v in account.summary().items():
print(f" {k}: {v}")
# Simulate price movement and margin call
price_scenarios = [1.0850, 1.0800, 1.0750, 1.0700, 1.0650, 1.0600]
print("\nPrice Movement Scenarios")
print("=" * 70)
print(f"{'Price':<10} {'P&L':<12} {'Equity':<12} {'Margin Level':<15} {'Status'}")
print("-" * 70)
for price in price_scenarios:
account.update_prices({'EUR/USD': price})
status = account.check_margin_call()
summary = account.summary()
pnl = summary['unrealized_pnl']
equity = summary['equity']
margin_lvl = summary['margin_level']
pnl_str = f"${pnl:+,.0f}"
equity_str = f"${equity:,.0f}"
margin_str = f"{margin_lvl}%" if margin_lvl != 'N/A' else 'N/A'
print(f"{price:<10} {pnl_str:<12} {equity_str:<12} {margin_str:<15} {status['status']}")
Exercise 3.1: Margin Calculator (Guided)
Complete the function to calculate margin requirements for different instruments.
Solution 3.1
def calculate_margin_requirements(
instrument_type: str,
position_value: float,
leverage: float = None
) -> dict:
"""
Calculate margin requirements for different instruments.
"""
# Default leverage by instrument type
default_leverage = {
'forex': 50,
'futures': 15,
'stocks': 2
}
# Get leverage (use default if not specified)
if leverage is None:
leverage = default_leverage.get(instrument_type.lower(), 1) # Get with default
# Calculate margin percentage
margin_pct = 100 / leverage # 100 divided by leverage
# Calculate initial margin
initial_margin = position_value * (margin_pct / 100)
# Maintenance margin (typically 50% of initial for forex)
maintenance_margin = initial_margin * 0.5 # 50% = 0.5
# Calculate max position from margin
max_position_from_margin = initial_margin * leverage
return {
'instrument': instrument_type,
'position_value': position_value,
'leverage': f"{leverage}:1",
'margin_pct': f"{margin_pct:.2f}%",
'initial_margin': round(initial_margin, 2),
'maintenance_margin': round(maintenance_margin, 2),
'max_adverse_move': f"{100/leverage:.2f}%"
}
3.3 Position Sizing with Leverage
The Risk Equation
Risk Amount = Account Balance × Risk Percentage
Position Size = Risk Amount / (Stop Loss in Pips × Pip Value)
Professional Position Sizing Rules
- Risk per trade: 1-2% of account
- Never risk more than you can afford to lose
- Account for correlation when holding multiple positions
- Leave margin buffer for adverse moves
class LeveragedPositionSizer:
"""
Calculate position sizes accounting for leverage.
Attributes:
account_balance: Trading account balance
leverage: Available leverage ratio
"""
def __init__(self, account_balance: float, leverage: float = 50):
self.account_balance = account_balance
self.leverage = leverage
@property
def max_position_value(self) -> float:
"""Maximum position value based on leverage."""
return self.account_balance * self.leverage
def size_by_risk(
self,
risk_pct: float,
stop_loss_pips: int,
pip_value: float = 10.0
) -> dict:
"""
Calculate position size based on risk.
Args:
risk_pct: Percentage of account to risk
stop_loss_pips: Stop loss distance in pips
pip_value: Dollar value per pip per lot
Returns:
Position sizing details
"""
# Calculate risk amount in dollars
risk_amount = self.account_balance * (risk_pct / 100)
# Calculate position size in lots
# Risk per lot = stop_loss_pips * pip_value
risk_per_lot = stop_loss_pips * pip_value
lots = risk_amount / risk_per_lot
# Calculate position value
position_value = lots * 100_000 # Standard lot = 100,000 units
# Check if within leverage limits
margin_required = position_value / self.leverage
within_limits = position_value <= self.max_position_value
return {
'risk_pct': risk_pct,
'risk_amount': round(risk_amount, 2),
'stop_loss_pips': stop_loss_pips,
'lots': round(lots, 2),
'units': int(lots * 100_000),
'position_value': round(position_value, 2),
'margin_required': round(margin_required, 2),
'margin_pct_of_account': round((margin_required / self.account_balance) * 100, 1),
'within_limits': within_limits
}
def size_by_margin(
self,
margin_pct: float,
current_price: float = 1.0
) -> dict:
"""
Calculate position size based on margin usage.
Args:
margin_pct: Percentage of account to use as margin
current_price: Current price for the instrument
Returns:
Position sizing details
"""
margin_used = self.account_balance * (margin_pct / 100)
position_value = margin_used * self.leverage
units = position_value / current_price
lots = units / 100_000
return {
'margin_pct': margin_pct,
'margin_used': round(margin_used, 2),
'position_value': round(position_value, 2),
'units': int(units),
'lots': round(lots, 2),
'remaining_margin': round(self.account_balance - margin_used, 2)
}
# Example position sizing
sizer = LeveragedPositionSizer(account_balance=10000, leverage=50)
print("Position Sizing Examples")
print("=" * 60)
print(f"Account: ${sizer.account_balance:,} | Leverage: {sizer.leverage}:1")
print(f"Max Position Value: ${sizer.max_position_value:,}")
# Size by risk
print("\n--- Size by Risk ---")
for risk in [1, 2, 3]:
result = sizer.size_by_risk(risk_pct=risk, stop_loss_pips=50)
print(f"\n{risk}% Risk, 50 pip stop:")
print(f" Risk Amount: ${result['risk_amount']}")
print(f" Position: {result['lots']} lots ({result['units']:,} units)")
print(f" Margin Required: ${result['margin_required']} ({result['margin_pct_of_account']}% of account)")
Exercise 3.2: Leveraged Position Sizing (Guided)
Complete the function to calculate optimal position size considering both risk and margin.
Solution 3.2
def calculate_safe_position(
account_balance: float,
leverage: float,
risk_pct: float,
stop_loss_pips: int,
max_margin_pct: float = 50.0,
pip_value: float = 10.0
) -> dict:
"""
Calculate safe position size considering both risk and margin limits.
"""
# Calculate position by risk
risk_amount = account_balance * (risk_pct / 100)
risk_per_lot = stop_loss_pips * pip_value
lots_by_risk = risk_amount / risk_per_lot # Divide by risk per lot
# Calculate position by margin limit
max_margin = account_balance * (max_margin_pct / 100)
max_position_value = max_margin * leverage # Multiply by leverage
lots_by_margin = max_position_value / 100_000 # Convert to lots
# Use the smaller of the two
final_lots = min(lots_by_risk, lots_by_margin) # Take minimum
limiting_factor = 'risk' if lots_by_risk < lots_by_margin else 'margin'
# Calculate final metrics
position_value = final_lots * 100_000
actual_margin = position_value / leverage
actual_risk = final_lots * risk_per_lot
return {
'lots_by_risk': round(lots_by_risk, 3),
'lots_by_margin': round(lots_by_margin, 3),
'final_lots': round(final_lots, 3),
'limiting_factor': limiting_factor,
'position_value': round(position_value, 2),
'margin_used': round(actual_margin, 2),
'margin_pct': round((actual_margin / account_balance) * 100, 1),
'actual_risk': round(actual_risk, 2),
'actual_risk_pct': round((actual_risk / account_balance) * 100, 2)
}
3.4 Leverage Risk Management
Why Most Traders Lose with Leverage
- Over-leveraging: Using too much of available leverage
- No stop losses: Letting losses run
- Ignoring volatility: Same leverage in all market conditions
- Correlation blindness: Multiple correlated positions multiply risk
Professional Approaches
| Approach | Description | Typical Leverage Used |
|---|---|---|
| Conservative | Long-term, low stress | 2:1 - 5:1 |
| Moderate | Balanced risk/reward | 5:1 - 15:1 |
| Aggressive | Active trading | 15:1 - 30:1 |
| Extreme (Not Recommended) | Day trading | 30:1+ |
class RiskScenarioAnalyzer:
"""
Analyze risk scenarios for leveraged positions.
Attributes:
account_balance: Starting balance
leverage: Leverage ratio
"""
def __init__(self, account_balance: float, leverage: float):
self.account_balance = account_balance
self.leverage = leverage
def simulate_price_path(
self,
position_pct: float,
daily_volatility: float,
days: int,
simulations: int = 1000
) -> pd.DataFrame:
"""
Simulate equity paths with leverage.
Args:
position_pct: Percentage of max leverage used
daily_volatility: Daily price volatility (e.g., 0.01 = 1%)
days: Number of days to simulate
simulations: Number of simulation paths
Returns:
DataFrame with simulation results
"""
# Calculate effective leverage used
effective_leverage = self.leverage * (position_pct / 100)
# Generate random returns
np.random.seed(42)
returns = np.random.normal(0, daily_volatility, (simulations, days))
# Apply leverage to returns
leveraged_returns = returns * effective_leverage
# Calculate equity paths
equity_paths = self.account_balance * np.cumprod(1 + leveraged_returns, axis=1)
# Identify blown accounts (equity <= 0)
blown = np.any(equity_paths <= 0, axis=1)
return {
'final_equities': equity_paths[:, -1],
'max_drawdowns': self._calculate_drawdowns(equity_paths),
'blown_accounts': blown.sum(),
'blow_up_rate': blown.mean() * 100,
'mean_final': equity_paths[:, -1].mean(),
'median_final': np.median(equity_paths[:, -1]),
'percentile_5': np.percentile(equity_paths[:, -1], 5),
'percentile_95': np.percentile(equity_paths[:, -1], 95)
}
def _calculate_drawdowns(self, equity_paths: np.ndarray) -> np.ndarray:
"""Calculate maximum drawdown for each path."""
running_max = np.maximum.accumulate(equity_paths, axis=1)
drawdowns = (running_max - equity_paths) / running_max
return np.max(drawdowns, axis=1)
def compare_leverage_levels(
self,
position_pcts: List[float],
daily_volatility: float = 0.01,
days: int = 252
) -> pd.DataFrame:
"""
Compare outcomes at different leverage levels.
Args:
position_pcts: List of position percentages to test
daily_volatility: Daily volatility
days: Simulation period
Returns:
Comparison DataFrame
"""
results = []
for pct in position_pcts:
sim = self.simulate_price_path(pct, daily_volatility, days)
effective_lev = self.leverage * (pct / 100)
results.append({
'Position %': f"{pct}%",
'Effective Leverage': f"{effective_lev:.1f}:1",
'Blow-up Rate': f"{sim['blow_up_rate']:.1f}%",
'Mean Final': f"${sim['mean_final']:,.0f}",
'Median Final': f"${sim['median_final']:,.0f}",
'5th Percentile': f"${sim['percentile_5']:,.0f}",
'Max DD (avg)': f"{np.mean(sim['max_drawdowns'])*100:.0f}%"
})
return pd.DataFrame(results)
# Analyze different leverage scenarios
analyzer = RiskScenarioAnalyzer(account_balance=10000, leverage=50)
print("Leverage Risk Comparison (1 Year, 1% Daily Volatility)")
print("=" * 90)
comparison = analyzer.compare_leverage_levels([10, 25, 50, 75, 100])
print(comparison.to_string(index=False))
# Visualize risk scenarios
def plot_leverage_risk():
"""Visualize the relationship between leverage and risk."""
analyzer = RiskScenarioAnalyzer(10000, 50)
position_pcts = [5, 10, 20, 30, 50, 75, 100]
blow_up_rates = []
median_returns = []
for pct in position_pcts:
sim = analyzer.simulate_price_path(pct, 0.01, 252, 500)
blow_up_rates.append(sim['blow_up_rate'])
median_returns.append((sim['median_final'] / 10000 - 1) * 100)
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
# Blow-up rate vs leverage
ax1 = axes[0]
effective_leverage = [50 * p / 100 for p in position_pcts]
ax1.bar(range(len(position_pcts)), blow_up_rates, color='red', alpha=0.7)
ax1.set_xticks(range(len(position_pcts)))
ax1.set_xticklabels([f"{l:.0f}:1" for l in effective_leverage])
ax1.set_xlabel('Effective Leverage')
ax1.set_ylabel('Account Blow-up Rate (%)')
ax1.set_title('Probability of Losing Everything (1 Year)')
ax1.grid(True, alpha=0.3)
# Add danger zone annotation
ax1.axhline(y=10, color='orange', linestyle='--', label='10% threshold')
ax1.legend()
# Risk-return tradeoff
ax2 = axes[1]
colors = ['green' if r > 0 else 'red' for r in median_returns]
ax2.bar(range(len(position_pcts)), median_returns, color=colors, alpha=0.7)
ax2.set_xticks(range(len(position_pcts)))
ax2.set_xticklabels([f"{l:.0f}:1" for l in effective_leverage])
ax2.set_xlabel('Effective Leverage')
ax2.set_ylabel('Median Annual Return (%)')
ax2.set_title('Median Return vs Leverage')
ax2.axhline(y=0, color='black', linestyle='-', linewidth=0.5)
ax2.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
plot_leverage_risk()
Exercise 3.3: Risk Scenarios (Guided)
Complete the function to analyze drawdown scenarios with leverage.
Solution 3.3
def analyze_drawdown_recovery(
initial_balance: float,
leverage: float,
drawdown_pct: float,
daily_return: float = 0.001
) -> dict:
"""
Analyze drawdown impact and recovery time with leverage.
"""
# Calculate balance after drawdown
balance_after = initial_balance * (1 - drawdown_pct / 100) # Convert pct to decimal
# Calculate loss amount
loss_amount = initial_balance - balance_after
# Calculate required return to recover
required_return = (initial_balance / balance_after - 1) * 100 # Convert to percentage
# Calculate price move that caused this (accounting for leverage)
price_move = drawdown_pct / leverage # Divide by leverage
# Estimate recovery days (simplified)
leveraged_daily_return = daily_return * leverage
if leveraged_daily_return > 0:
recovery_days = np.log(initial_balance / balance_after) / np.log(1 + leveraged_daily_return)
else:
recovery_days = float('inf')
return {
'initial_balance': initial_balance,
'drawdown_pct': f"{drawdown_pct}%",
'balance_after': round(balance_after, 2),
'loss_amount': round(loss_amount, 2),
'price_move_caused': f"{price_move:.2f}%",
'required_return_to_recover': f"{required_return:.1f}%",
'estimated_recovery_days': int(recovery_days) if recovery_days != float('inf') else 'N/A'
}
Exercise 3.4: Margin Account Simulator (Open-ended)
Build a margin account simulator that tracks positions, margin, and handles margin calls.
Your implementation:
Solution 3.4
class MarginAccountSimulator:
"""
Full margin account simulator with liquidation.
"""
def __init__(
self,
balance: float,
leverage: float = 50,
maintenance_pct: float = 50
):
self.initial_balance = balance
self.cash = balance
self.leverage = leverage
self.initial_margin_pct = 100 / leverage
self.maintenance_pct = maintenance_pct
self.positions = {}
self.position_counter = 0
self.trade_history = []
@property
def unrealized_pnl(self) -> float:
return sum(p['unrealized_pnl'] for p in self.positions.values())
@property
def equity(self) -> float:
return self.cash + self.unrealized_pnl
@property
def used_margin(self) -> float:
return sum(p['margin'] for p in self.positions.values())
@property
def margin_level(self) -> float:
if self.used_margin == 0:
return float('inf')
return (self.equity / self.used_margin) * 100
def open_position(self, symbol: str, direction: str, size: float, price: float) -> dict:
position_value = size * price
margin_required = position_value * (self.initial_margin_pct / 100)
if margin_required > (self.equity - self.used_margin):
return {'error': 'Insufficient margin'}
self.position_counter += 1
position = {
'id': self.position_counter,
'symbol': symbol,
'direction': direction,
'size': size,
'entry_price': price,
'current_price': price,
'margin': margin_required,
'unrealized_pnl': 0
}
self.positions[self.position_counter] = position
return position
def close_position(self, position_id: int, price: float) -> dict:
if position_id not in self.positions:
return {'error': 'Position not found'}
pos = self.positions[position_id]
pos['current_price'] = price
pnl = (price - pos['entry_price']) * pos['size']
if pos['direction'] == 'short':
pnl = -pnl
self.cash += pnl
del self.positions[position_id]
self.trade_history.append({
'position_id': position_id,
'symbol': pos['symbol'],
'realized_pnl': pnl
})
return {'realized_pnl': pnl}
def update_prices(self, prices: Dict[str, float]):
for pos in self.positions.values():
if pos['symbol'] in prices:
pos['current_price'] = prices[pos['symbol']]
pnl = (pos['current_price'] - pos['entry_price']) * pos['size']
if pos['direction'] == 'short':
pnl = -pnl
pos['unrealized_pnl'] = pnl
def check_margin_status(self) -> dict:
margin_call_level = 100 # 100% margin level = margin call
liquidation_level = 50 # 50% margin level = liquidation
if self.margin_level <= liquidation_level:
return {'status': 'LIQUIDATION', 'margin_level': self.margin_level}
elif self.margin_level <= margin_call_level:
return {'status': 'MARGIN_CALL', 'margin_level': self.margin_level}
else:
return {'status': 'OK', 'margin_level': self.margin_level}
def liquidate_if_needed(self) -> List[dict]:
status = self.check_margin_status()
liquidated = []
if status['status'] == 'LIQUIDATION':
# Close all positions
for pos_id in list(self.positions.keys()):
pos = self.positions[pos_id]
result = self.close_position(pos_id, pos['current_price'])
liquidated.append({
'position_id': pos_id,
'symbol': pos['symbol'],
'pnl': result.get('realized_pnl', 0)
})
return liquidated
# Test the simulator
sim = MarginAccountSimulator(10000, leverage=50)
sim.open_position('EUR/USD', 'long', 100000, 1.0850)
# Simulate price drop
for price in [1.0850, 1.0800, 1.0750, 1.0700]:
sim.update_prices({'EUR/USD': price})
status = sim.check_margin_status()
print(f"Price: {price} | Equity: ${sim.equity:,.0f} | "
f"Margin Level: {sim.margin_level:.0f}% | Status: {status['status']}")
liq = sim.liquidate_if_needed()
if liq:
print(f" LIQUIDATED: {liq}")
break
Exercise 3.5: Portfolio Leverage Manager (Open-ended)
Build a portfolio leverage manager that tracks total exposure across multiple positions.
Your implementation:
Solution 3.5
class PortfolioLeverageManager:
"""
Manage leverage across a portfolio of positions.
"""
def __init__(
self,
account_balance: float,
max_total_leverage: float = 10,
max_correlated_leverage: float = 5
):
self.account_balance = account_balance
self.max_total_leverage = max_total_leverage
self.max_correlated_leverage = max_correlated_leverage
self.positions = []
def add_position(self, symbol: str, value: float, correlation_group: str):
self.positions.append({
'symbol': symbol,
'value': value,
'correlation_group': correlation_group
})
def get_total_leverage(self) -> float:
total_value = sum(p['value'] for p in self.positions)
return total_value / self.account_balance
def get_correlated_exposure(self, group: str) -> float:
group_value = sum(
p['value'] for p in self.positions
if p['correlation_group'] == group
)
return group_value / self.account_balance
def recommend_new_position(self, symbol: str, desired_value: float, group: str) -> dict:
current_leverage = self.get_total_leverage()
new_leverage = current_leverage + (desired_value / self.account_balance)
current_group = self.get_correlated_exposure(group)
new_group = current_group + (desired_value / self.account_balance)
# Check limits
total_ok = new_leverage <= self.max_total_leverage
group_ok = new_group <= self.max_correlated_leverage
if total_ok and group_ok:
recommendation = 'APPROVED'
suggested_value = desired_value
else:
recommendation = 'REDUCE'
# Calculate max allowed
max_by_total = (self.max_total_leverage - current_leverage) * self.account_balance
max_by_group = (self.max_correlated_leverage - current_group) * self.account_balance
suggested_value = max(0, min(max_by_total, max_by_group))
return {
'symbol': symbol,
'requested': desired_value,
'recommendation': recommendation,
'suggested_value': round(suggested_value, 2),
'new_total_leverage': round(new_leverage, 2),
'new_group_leverage': round(new_group, 2)
}
def risk_report(self) -> pd.DataFrame:
groups = set(p['correlation_group'] for p in self.positions)
results = []
for group in groups:
group_positions = [p for p in self.positions if p['correlation_group'] == group]
group_value = sum(p['value'] for p in group_positions)
results.append({
'Group': group,
'Positions': len(group_positions),
'Total Value': f"${group_value:,.0f}",
'Leverage': f"{group_value/self.account_balance:.1f}x",
'Status': 'OK' if group_value/self.account_balance <= self.max_correlated_leverage else 'WARNING'
})
return pd.DataFrame(results)
# Test
manager = PortfolioLeverageManager(10000, max_total_leverage=10, max_correlated_leverage=5)
manager.add_position('EUR/USD', 30000, 'USD')
manager.add_position('GBP/USD', 20000, 'USD')
manager.add_position('USD/JPY', 15000, 'JPY')
print("Portfolio Risk Report")
print(manager.risk_report().to_string(index=False))
print(f"\nTotal Leverage: {manager.get_total_leverage():.1f}x")
# Check new position
rec = manager.recommend_new_position('AUD/USD', 30000, 'USD')
print(f"\nNew Position Recommendation: {rec}")
Exercise 3.6: Leverage Risk Calculator (Open-ended)
Build a comprehensive leverage risk calculator for pre-trade analysis.
Your implementation:
Solution 3.6
class LeverageRiskCalculator:
"""
Pre-trade leverage risk analysis.
"""
def __init__(self):
self.account = None
self.leverage = None
self.volatility = None
def set_parameters(self, account: float, leverage: float, volatility: float):
self.account = account
self.leverage = leverage
self.volatility = volatility
def calculate_max_position(self) -> dict:
max_position = self.account * self.leverage
margin_required = self.account # Using 100% of account as margin
wipeout_move = 100 / self.leverage
# Safe position (account for 3-sigma move)
three_sigma = self.volatility * 3
safe_leverage = min(self.leverage, 100 / (three_sigma * 100) * 0.5)
safe_position = self.account * safe_leverage
return {
'max_position': round(max_position, 2),
'margin_required': round(margin_required, 2),
'wipeout_move_pct': round(wipeout_move, 2),
'recommended_leverage': round(safe_leverage, 1),
'safe_position': round(safe_position, 2),
'daily_var_95': round(self.volatility * 1.65 * safe_leverage * self.account, 2)
}
def margin_call_price(self, entry_price: float, direction: str) -> float:
margin_pct = 100 / self.leverage
maintenance_pct = margin_pct * 0.5 # 50% maintenance
# Price move to hit maintenance
move_to_maintenance = (margin_pct - maintenance_pct) / 100
if direction == 'long':
return entry_price * (1 - move_to_maintenance)
else:
return entry_price * (1 + move_to_maintenance)
def scenario_analysis(self, position_value: float, scenarios: List[float]) -> pd.DataFrame:
results = []
margin = position_value / self.leverage
for change_pct in scenarios:
pnl = position_value * (change_pct / 100)
new_equity = self.account + pnl
margin_level = (new_equity / margin) * 100 if margin > 0 else 0
results.append({
'Price Change': f"{change_pct:+.1f}%",
'P&L': f"${pnl:+,.0f}",
'Equity': f"${new_equity:,.0f}",
'Margin Level': f"{margin_level:.0f}%",
'Status': 'OK' if margin_level > 100 else 'MARGIN CALL' if margin_level > 50 else 'LIQUIDATION'
})
return pd.DataFrame(results)
def plot_risk_profile(self):
if self.account is None:
print("Set parameters first")
return
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
# Left: Equity vs price change
ax1 = axes[0]
price_changes = np.linspace(-5, 5, 100)
for lev in [5, 10, 25, 50]:
position = self.account * lev
equity = self.account + position * (price_changes / 100)
ax1.plot(price_changes, equity / 1000, label=f'{lev}:1')
ax1.axhline(y=0, color='red', linestyle='--', alpha=0.7)
ax1.set_xlabel('Price Change (%)')
ax1.set_ylabel('Equity ($1000s)')
ax1.set_title('Equity vs Price Change at Different Leverage')
ax1.legend()
ax1.grid(True, alpha=0.3)
# Right: VaR by leverage
ax2 = axes[1]
leverage_range = range(1, 51)
var_95 = [self.volatility * 1.65 * l * self.account for l in leverage_range]
ax2.fill_between(leverage_range, var_95, alpha=0.3, color='red')
ax2.plot(leverage_range, var_95, color='red', linewidth=2)
ax2.axhline(y=self.account, color='black', linestyle='--', label='Account Balance')
ax2.set_xlabel('Leverage')
ax2.set_ylabel('Daily VaR (95%)')
ax2.set_title('Value at Risk vs Leverage')
ax2.legend()
ax2.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
# Test
calc = LeverageRiskCalculator()
calc.set_parameters(account=10000, leverage=50, volatility=0.01)
print("Max Position Analysis")
for k, v in calc.calculate_max_position().items():
print(f" {k}: {v}")
print(f"\nMargin Call Price (Long from 1.0850): {calc.margin_call_price(1.0850, 'long'):.4f}")
print("\nScenario Analysis (Position: $200,000)")
print(calc.scenario_analysis(200000, [-3, -2, -1, 0, 1, 2, 3]).to_string(index=False))
calc.plot_risk_profile()
Module Project: Leverage Risk Calculator
Build a comprehensive leverage risk management system.
class LeverageRiskManager:
"""
Comprehensive leverage and risk management system.
Combines margin calculation, position sizing, scenario analysis,
and risk visualization for leveraged trading.
"""
def __init__(
self,
account_balance: float,
max_leverage: float = 50,
risk_per_trade: float = 2.0,
max_portfolio_risk: float = 10.0
):
self.account_balance = account_balance
self.max_leverage = max_leverage
self.risk_per_trade = risk_per_trade
self.max_portfolio_risk = max_portfolio_risk
self.positions = []
# ==================== Position Sizing ====================
def calculate_position_size(
self,
stop_loss_pips: int,
pip_value: float = 10.0,
custom_risk_pct: float = None
) -> dict:
"""
Calculate optimal position size.
Args:
stop_loss_pips: Stop loss distance in pips
pip_value: Value per pip per lot
custom_risk_pct: Override default risk percentage
Returns:
Position sizing details
"""
risk_pct = custom_risk_pct or self.risk_per_trade
# Calculate by risk
risk_amount = self.account_balance * (risk_pct / 100)
risk_per_lot = stop_loss_pips * pip_value
lots_by_risk = risk_amount / risk_per_lot
# Calculate by leverage limit
max_position_value = self.account_balance * self.max_leverage
lots_by_leverage = max_position_value / 100_000
# Use conservative sizing
recommended_lots = min(lots_by_risk, lots_by_leverage)
position_value = recommended_lots * 100_000
margin_required = position_value / self.max_leverage
effective_leverage = position_value / self.account_balance
return {
'risk_pct': risk_pct,
'risk_amount': round(risk_amount, 2),
'stop_loss_pips': stop_loss_pips,
'lots': round(recommended_lots, 3),
'units': int(recommended_lots * 100_000),
'position_value': round(position_value, 2),
'margin_required': round(margin_required, 2),
'effective_leverage': round(effective_leverage, 1),
'margin_pct_of_account': round((margin_required / self.account_balance) * 100, 1)
}
# ==================== Margin Analysis ====================
def calculate_margin_call_level(
self,
entry_price: float,
position_value: float,
direction: str = 'long',
maintenance_pct: float = 50
) -> dict:
"""
Calculate margin call price level.
Args:
entry_price: Position entry price
position_value: Total position value
direction: 'long' or 'short'
maintenance_pct: Maintenance margin percentage
Returns:
Margin call analysis
"""
margin_used = position_value / self.max_leverage
maintenance_margin = margin_used * (maintenance_pct / 100)
# Calculate price move to margin call
equity_buffer = self.account_balance - maintenance_margin
price_move_pct = (equity_buffer / position_value) * 100
if direction == 'long':
margin_call_price = entry_price * (1 - price_move_pct / 100)
else:
margin_call_price = entry_price * (1 + price_move_pct / 100)
# Calculate liquidation price (assume 20% maintenance)
liquidation_buffer = self.account_balance - margin_used * 0.2
liq_move_pct = (liquidation_buffer / position_value) * 100
if direction == 'long':
liquidation_price = entry_price * (1 - liq_move_pct / 100)
else:
liquidation_price = entry_price * (1 + liq_move_pct / 100)
return {
'entry_price': entry_price,
'direction': direction,
'position_value': position_value,
'margin_used': round(margin_used, 2),
'margin_call_price': round(margin_call_price, 5),
'margin_call_pips': abs(round((entry_price - margin_call_price) / 0.0001)),
'liquidation_price': round(liquidation_price, 5),
'buffer_pct': round(price_move_pct, 2)
}
# ==================== Scenario Analysis ====================
def scenario_analysis(
self,
position_value: float,
price_scenarios: List[float] = None
) -> pd.DataFrame:
"""
Analyze P&L under different price scenarios.
Args:
position_value: Total position value
price_scenarios: List of price change percentages
Returns:
Scenario analysis DataFrame
"""
if price_scenarios is None:
price_scenarios = [-5, -3, -2, -1, -0.5, 0, 0.5, 1, 2, 3, 5]
margin_used = position_value / self.max_leverage
results = []
for change_pct in price_scenarios:
pnl = position_value * (change_pct / 100)
new_equity = self.account_balance + pnl
margin_level = (new_equity / margin_used) * 100 if margin_used > 0 else float('inf')
account_change = (pnl / self.account_balance) * 100
if margin_level <= 20:
status = 'LIQUIDATION'
elif margin_level <= 50:
status = 'MARGIN CALL'
elif margin_level <= 100:
status = 'WARNING'
else:
status = 'OK'
results.append({
'Price Change': f"{change_pct:+.1f}%",
'P&L': f"${pnl:+,.0f}",
'Account Change': f"{account_change:+.1f}%",
'Equity': f"${new_equity:,.0f}",
'Margin Level': f"{margin_level:.0f}%",
'Status': status
})
return pd.DataFrame(results)
# ==================== Risk Metrics ====================
def calculate_risk_metrics(
self,
position_value: float,
daily_volatility: float = 0.01
) -> dict:
"""
Calculate risk metrics for a position.
Args:
position_value: Total position value
daily_volatility: Expected daily volatility
Returns:
Risk metrics dictionary
"""
effective_leverage = position_value / self.account_balance
# Daily VaR (95%)
var_95 = daily_volatility * 1.65 * position_value
# Daily VaR (99%)
var_99 = daily_volatility * 2.33 * position_value
# Expected daily P&L range
daily_range = daily_volatility * position_value
# Days to potential wipeout (99% VaR)
if var_99 > 0:
days_to_wipeout = self.account_balance / var_99
else:
days_to_wipeout = float('inf')
# Price move to lose entire account
wipeout_move = (self.account_balance / position_value) * 100
return {
'position_value': position_value,
'effective_leverage': round(effective_leverage, 1),
'daily_volatility': f"{daily_volatility*100:.1f}%",
'daily_var_95': round(var_95, 2),
'daily_var_95_pct': round((var_95 / self.account_balance) * 100, 1),
'daily_var_99': round(var_99, 2),
'expected_daily_range': round(daily_range, 2),
'wipeout_price_move': round(wipeout_move, 2),
'theoretical_days_to_wipeout': round(days_to_wipeout, 1)
}
# ==================== Visualization ====================
def plot_risk_profile(self, position_value: float = None):
"""Visualize risk profile."""
if position_value is None:
position_value = self.account_balance * 10 # Default 10:1
fig, axes = plt.subplots(2, 2, figsize=(14, 10))
# 1. Equity curve by price change
ax1 = axes[0, 0]
price_changes = np.linspace(-5, 5, 100)
equity = self.account_balance + position_value * (price_changes / 100)
ax1.fill_between(price_changes, equity, self.account_balance,
where=equity > self.account_balance, alpha=0.3, color='green')
ax1.fill_between(price_changes, equity, self.account_balance,
where=equity < self.account_balance, alpha=0.3, color='red')
ax1.plot(price_changes, equity, 'b-', linewidth=2)
ax1.axhline(y=0, color='red', linestyle='--', label='Wipeout')
ax1.axhline(y=self.account_balance, color='black', linestyle='-', alpha=0.3)
ax1.set_xlabel('Price Change (%)')
ax1.set_ylabel('Account Equity ($)')
ax1.set_title(f'Equity vs Price Change (Position: ${position_value:,.0f})')
ax1.legend()
ax1.grid(True, alpha=0.3)
# 2. Leverage comparison
ax2 = axes[0, 1]
leverage_levels = [5, 10, 25, 50]
colors = ['green', 'blue', 'orange', 'red']
for lev, color in zip(leverage_levels, colors):
pos_val = self.account_balance * lev
eq = self.account_balance + pos_val * (price_changes / 100)
ax2.plot(price_changes, eq / 1000, label=f'{lev}:1', color=color, linewidth=2)
ax2.axhline(y=0, color='red', linestyle='--', alpha=0.7)
ax2.set_xlabel('Price Change (%)')
ax2.set_ylabel('Equity ($1000s)')
ax2.set_title('Equity by Leverage Level')
ax2.legend()
ax2.grid(True, alpha=0.3)
# 3. VaR by leverage
ax3 = axes[1, 0]
leverage_range = range(1, 51)
daily_vol = 0.01
var_95 = [daily_vol * 1.65 * self.account_balance * l for l in leverage_range]
var_99 = [daily_vol * 2.33 * self.account_balance * l for l in leverage_range]
ax3.fill_between(leverage_range, var_99, alpha=0.3, color='red', label='99% VaR')
ax3.fill_between(leverage_range, var_95, alpha=0.3, color='orange', label='95% VaR')
ax3.axhline(y=self.account_balance, color='black', linestyle='--',
label='Account Balance')
ax3.set_xlabel('Leverage')
ax3.set_ylabel('Daily VaR ($)')
ax3.set_title('Value at Risk vs Leverage (1% daily vol)')
ax3.legend()
ax3.grid(True, alpha=0.3)
# 4. Margin level by price change
ax4 = axes[1, 1]
margin_used = position_value / self.max_leverage
margin_levels = ((self.account_balance + position_value * (price_changes / 100))
/ margin_used) * 100
ax4.plot(price_changes, margin_levels, 'b-', linewidth=2)
ax4.axhline(y=100, color='orange', linestyle='--', label='Margin Call (100%)')
ax4.axhline(y=50, color='red', linestyle='--', label='Stop Out (50%)')
ax4.fill_between(price_changes, margin_levels, 0,
where=margin_levels < 50, alpha=0.3, color='red')
ax4.fill_between(price_changes, margin_levels, 0,
where=(margin_levels >= 50) & (margin_levels < 100),
alpha=0.3, color='orange')
ax4.set_xlabel('Price Change (%)')
ax4.set_ylabel('Margin Level (%)')
ax4.set_title('Margin Level vs Price Change')
ax4.legend()
ax4.grid(True, alpha=0.3)
ax4.set_ylim(0, 500)
plt.tight_layout()
plt.show()
# ==================== Summary ====================
def print_analysis(
self,
stop_loss_pips: int = 50,
entry_price: float = 1.0850
):
"""Print comprehensive analysis."""
print("\n" + "=" * 70)
print("LEVERAGE RISK ANALYSIS")
print("=" * 70)
print(f"\nAccount Balance: ${self.account_balance:,}")
print(f"Max Leverage: {self.max_leverage}:1")
print(f"Risk Per Trade: {self.risk_per_trade}%")
# Position sizing
print("\n" + "-" * 70)
print("POSITION SIZING")
print("-" * 70)
sizing = self.calculate_position_size(stop_loss_pips)
for k, v in sizing.items():
print(f" {k}: {v}")
# Margin call levels
print("\n" + "-" * 70)
print("MARGIN CALL ANALYSIS")
print("-" * 70)
margin_info = self.calculate_margin_call_level(
entry_price, sizing['position_value'], 'long'
)
for k, v in margin_info.items():
print(f" {k}: {v}")
# Risk metrics
print("\n" + "-" * 70)
print("RISK METRICS")
print("-" * 70)
metrics = self.calculate_risk_metrics(sizing['position_value'])
for k, v in metrics.items():
print(f" {k}: {v}")
# Scenario analysis
print("\n" + "-" * 70)
print("SCENARIO ANALYSIS")
print("-" * 70)
scenarios = self.scenario_analysis(sizing['position_value'])
print(scenarios.to_string(index=False))
print("\n" + "=" * 70)
# Demonstrate the Leverage Risk Manager
manager = LeverageRiskManager(
account_balance=10000,
max_leverage=50,
risk_per_trade=2.0,
max_portfolio_risk=10.0
)
# Print comprehensive analysis
manager.print_analysis(stop_loss_pips=50, entry_price=1.0850)
# Visualize risk profile
manager.plot_risk_profile(position_value=100000)
Key Takeaways
- Leverage amplifies both gains and losses - a 1% price move with 50:1 leverage is a 50% account change
- Margin is the collateral required to open and maintain positions
- Initial margin opens positions; maintenance margin keeps them open
- Margin calls occur when equity falls below maintenance level - action required or positions get liquidated
- Position sizing should be based on risk (% of account), not margin available
- Professional traders typically use 5:1 to 15:1 effective leverage, not the maximum available
- Higher leverage = smaller adverse move to wipeout (50:1 = 2% move, 100:1 = 1% move)
- Recovery math is brutal - a 50% loss requires a 100% gain to recover
Next: Module 4 - Data & Tools
Module 4: Data & Tools
Part 1: Market Fundamentals
| Duration | Exercises |
|---|---|
| ~2.5 hours | 6 |
Learning Objectives
By the end of this module, you will be able to:
- Access forex data from multiple sources
- Set up and use the OANDA API for forex data
- Download and store historical price data
- Work with real-time streaming data
- Build robust data pipelines for forex trading
# Standard imports for this module
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from datetime import datetime, timedelta, timezone
from typing import Dict, List, Optional, Tuple, Generator
from dataclasses import dataclass
import json
import time
from pathlib import Path
# Display settings
pd.set_option('display.max_columns', 15)
pd.set_option('display.width', 200)
plt.style.use('seaborn-v0_8-whitegrid')
4.1 Forex Data Sources
Available Data Sources
| Source | Type | Cost | Best For |
|---|---|---|---|
| OANDA | Broker API | Free (demo) | Live trading, historical |
| FXCM | Broker API | Free (demo) | Historical tick data |
| Alpha Vantage | Data API | Free tier | Basic forex data |
| Yahoo Finance | Data API | Free | Currency pair proxies |
| Dukascopy | Data Download | Free | Tick data backtesting |
| TradingView | Charts | Free/Paid | Charting, basic data |
Data Types
- Tick Data: Every price change (bid/ask)
- OHLC Bars: Open, High, Low, Close aggregated by time
- Order Book: Depth of market (advanced)
@dataclass
class ForexDataSource:
"""
Represents a forex data source.
Attributes:
name: Source name
api_type: Type of API/access
data_types: Available data types
free_tier: Whether free access is available
"""
name: str
api_type: str
data_types: List[str]
free_tier: bool
url: str
# Define available data sources
DATA_SOURCES = {
'oanda': ForexDataSource(
name='OANDA',
api_type='REST/Streaming',
data_types=['OHLC', 'Tick', 'Order Book'],
free_tier=True,
url='https://developer.oanda.com'
),
'fxcm': ForexDataSource(
name='FXCM',
api_type='REST/Socket',
data_types=['OHLC', 'Tick'],
free_tier=True,
url='https://fxcm.github.io/rest-api-docs/'
),
'alpha_vantage': ForexDataSource(
name='Alpha Vantage',
api_type='REST',
data_types=['OHLC'],
free_tier=True,
url='https://www.alphavantage.co'
),
'yahoo': ForexDataSource(
name='Yahoo Finance',
api_type='REST (yfinance)',
data_types=['OHLC'],
free_tier=True,
url='https://finance.yahoo.com'
),
'dukascopy': ForexDataSource(
name='Dukascopy',
api_type='Download',
data_types=['Tick', 'OHLC'],
free_tier=True,
url='https://www.dukascopy.com/swiss/english/marketwatch/historical/'
)
}
# Display sources
print("Forex Data Sources")
print("=" * 70)
for key, source in DATA_SOURCES.items():
print(f"\n{source.name}")
print(f" API Type: {source.api_type}")
print(f" Data Types: {', '.join(source.data_types)}")
print(f" Free Tier: {'Yes' if source.free_tier else 'No'}")
print(f" URL: {source.url}")
# Mock forex data generator (simulates API response)
class MockForexDataProvider:
"""
Mock data provider for demonstration.
In production, this would be replaced with actual API calls.
"""
# Base prices for common pairs
BASE_PRICES = {
'EUR_USD': 1.0850,
'GBP_USD': 1.2650,
'USD_JPY': 150.50,
'AUD_USD': 0.6550,
'USD_CAD': 1.3650,
'EUR_GBP': 0.8580,
'EUR_JPY': 163.30
}
# Typical spreads in pips
TYPICAL_SPREADS = {
'EUR_USD': 0.8,
'GBP_USD': 1.2,
'USD_JPY': 1.0,
'AUD_USD': 1.0,
'USD_CAD': 1.5,
'EUR_GBP': 1.5,
'EUR_JPY': 1.8
}
def __init__(self, seed: int = 42):
self.rng = np.random.default_rng(seed)
def get_current_price(self, pair: str) -> dict:
"""
Get current bid/ask price for a pair.
Args:
pair: Currency pair (e.g., 'EUR_USD')
Returns:
Price dictionary with bid, ask, spread
"""
pair = pair.upper().replace('/', '_')
base_price = self.BASE_PRICES.get(pair, 1.0)
spread_pips = self.TYPICAL_SPREADS.get(pair, 2.0)
# Add some randomness
pip_size = 0.01 if 'JPY' in pair else 0.0001
noise = self.rng.normal(0, 5) * pip_size
mid_price = base_price + noise
half_spread = (spread_pips / 2) * pip_size
return {
'pair': pair,
'bid': round(mid_price - half_spread, 5),
'ask': round(mid_price + half_spread, 5),
'mid': round(mid_price, 5),
'spread_pips': spread_pips,
'timestamp': datetime.now(timezone.utc).isoformat()
}
def get_historical_data(
self,
pair: str,
granularity: str = 'H1',
count: int = 100
) -> pd.DataFrame:
"""
Get historical OHLC data.
Args:
pair: Currency pair
granularity: Timeframe (M1, M5, M15, H1, H4, D)
count: Number of candles
Returns:
DataFrame with OHLC data
"""
pair = pair.upper().replace('/', '_')
base_price = self.BASE_PRICES.get(pair, 1.0)
pip_size = 0.01 if 'JPY' in pair else 0.0001
# Determine frequency
freq_map = {
'M1': '1min', 'M5': '5min', 'M15': '15min',
'H1': '1h', 'H4': '4h', 'D': '1D'
}
freq = freq_map.get(granularity, '1h')
# Generate timestamps
end_time = datetime.now(timezone.utc)
dates = pd.date_range(end=end_time, periods=count, freq=freq)
# Generate price path
returns = self.rng.normal(0, 0.0002, count) # Small returns
close_prices = base_price * np.cumprod(1 + returns)
# Generate OHLC
volatility = 20 * pip_size # Typical range
df = pd.DataFrame({
'open': np.roll(close_prices, 1),
'high': close_prices + self.rng.uniform(0, volatility, count),
'low': close_prices - self.rng.uniform(0, volatility, count),
'close': close_prices,
'volume': self.rng.integers(1000, 10000, count)
}, index=dates)
df.iloc[0, 0] = base_price # Fix first open
# Ensure OHLC relationships
df['high'] = df[['open', 'high', 'close']].max(axis=1)
df['low'] = df[['open', 'low', 'close']].min(axis=1)
return df.round(5)
# Demonstrate the mock provider
provider = MockForexDataProvider()
print("Current Prices")
print("=" * 60)
for pair in ['EUR_USD', 'GBP_USD', 'USD_JPY']:
price = provider.get_current_price(pair)
print(f"{price['pair']:10} Bid: {price['bid']:.5f} | Ask: {price['ask']:.5f} | "
f"Spread: {price['spread_pips']} pips")
4.2 OANDA API Setup
OANDA provides one of the most popular APIs for forex trading. Here's how to set it up:
Account Setup
- Create a demo account at OANDA
- Navigate to "Manage API Access" in your account settings
- Generate an API token
- Note your account ID
API Endpoints
| Endpoint | Purpose |
|---|---|
/v3/accounts |
Account information |
/v3/instruments/{pair}/candles |
Historical OHLC |
/v3/accounts/{id}/pricing/stream |
Real-time prices |
/v3/accounts/{id}/orders |
Order management |
class OANDAClient:
"""
OANDA API client for forex data and trading.
This is a mock implementation for educational purposes.
In production, use the oandapyV20 library.
Attributes:
account_id: OANDA account ID
access_token: API access token
environment: 'practice' or 'live'
"""
ENVIRONMENTS = {
'practice': 'https://api-fxpractice.oanda.com',
'live': 'https://api-fxtrade.oanda.com'
}
GRANULARITIES = {
'S5': 'S5', 'S10': 'S10', 'S15': 'S15', 'S30': 'S30',
'M1': 'M1', 'M2': 'M2', 'M4': 'M4', 'M5': 'M5',
'M10': 'M10', 'M15': 'M15', 'M30': 'M30',
'H1': 'H1', 'H2': 'H2', 'H3': 'H3', 'H4': 'H4',
'H6': 'H6', 'H8': 'H8', 'H12': 'H12',
'D': 'D', 'W': 'W', 'M': 'M'
}
def __init__(
self,
account_id: str,
access_token: str,
environment: str = 'practice'
):
self.account_id = account_id
self.access_token = access_token
self.environment = environment
self.base_url = self.ENVIRONMENTS[environment]
# Use mock provider for demonstration
self._mock = MockForexDataProvider()
def _make_request(self, endpoint: str, params: dict = None) -> dict:
"""
Make API request (mock implementation).
In production, this would use requests library.
"""
# Mock response based on endpoint
return {'status': 'ok'}
def get_account_summary(self) -> dict:
"""
Get account summary.
Returns:
Account details including balance, margin, etc.
"""
# Mock account summary
return {
'account_id': self.account_id,
'balance': 10000.00,
'unrealized_pl': 125.50,
'nav': 10125.50,
'margin_used': 500.00,
'margin_available': 9625.50,
'open_positions': 2,
'open_trades': 3
}
def get_instruments(self) -> List[dict]:
"""
Get available trading instruments.
Returns:
List of tradeable instruments
"""
instruments = [
{'name': 'EUR_USD', 'type': 'CURRENCY', 'pip': 0.0001},
{'name': 'GBP_USD', 'type': 'CURRENCY', 'pip': 0.0001},
{'name': 'USD_JPY', 'type': 'CURRENCY', 'pip': 0.01},
{'name': 'AUD_USD', 'type': 'CURRENCY', 'pip': 0.0001},
{'name': 'USD_CAD', 'type': 'CURRENCY', 'pip': 0.0001},
{'name': 'EUR_GBP', 'type': 'CURRENCY', 'pip': 0.0001},
{'name': 'EUR_JPY', 'type': 'CURRENCY', 'pip': 0.01},
{'name': 'XAU_USD', 'type': 'METAL', 'pip': 0.01},
]
return instruments
def get_pricing(self, instruments: List[str]) -> List[dict]:
"""
Get current pricing for instruments.
Args:
instruments: List of instrument names
Returns:
List of price dictionaries
"""
prices = []
for inst in instruments:
prices.append(self._mock.get_current_price(inst))
return prices
def get_candles(
self,
instrument: str,
granularity: str = 'H1',
count: int = 100,
from_time: datetime = None,
to_time: datetime = None
) -> pd.DataFrame:
"""
Get historical candle data.
Args:
instrument: Currency pair
granularity: Timeframe
count: Number of candles
from_time: Start time
to_time: End time
Returns:
DataFrame with OHLC data
"""
return self._mock.get_historical_data(instrument, granularity, count)
# Demonstrate OANDA client
client = OANDAClient(
account_id='101-001-12345678-001',
access_token='your-api-token-here',
environment='practice'
)
print("OANDA Account Summary")
print("=" * 50)
summary = client.get_account_summary()
for k, v in summary.items():
print(f"{k}: {v}")
print("\nAvailable Instruments")
print("=" * 50)
instruments = client.get_instruments()
for inst in instruments[:5]:
print(f"{inst['name']:10} Type: {inst['type']:10} Pip: {inst['pip']}")
Exercise 4.1: Connect to OANDA (Guided)
Complete the function to set up an OANDA connection and fetch basic data.
Solution 4.1
def setup_oanda_connection(
account_id: str,
access_token: str,
environment: str = 'practice'
) -> dict:
"""
Set up OANDA connection and verify access.
"""
# Create client
client = OANDAClient(
account_id=account_id,
access_token=access_token,
environment=environment # Pass the environment parameter
)
# Get account summary
account = client.get_account_summary() # Call get_account_summary method
# Get available instruments
instruments = client.get_instruments()
# Get sample pricing
sample_pairs = ['EUR_USD', 'GBP_USD', 'USD_JPY']
prices = client.get_pricing(sample_pairs) # Call get_pricing method
return {
'status': 'connected',
'environment': environment,
'account_id': account_id,
'balance': account['balance'],
'nav': account['nav'],
'instruments_available': len(instruments),
'sample_prices': prices
}
4.3 Historical Data
Downloading and Storing Historical Data
For backtesting and analysis, you need historical price data. Key considerations:
- Timeframe: Choose appropriate granularity (M1 for scalping, H1 for swing)
- Date Range: Ensure sufficient history for your strategy
- Data Quality: Check for gaps, outliers, weekend data
- Storage Format: CSV, Parquet, or database
class ForexDataManager:
"""
Manages downloading, storing, and retrieving forex data.
Attributes:
data_dir: Directory for storing data
client: OANDA client for fetching data
"""
def __init__(self, data_dir: str = './forex_data', client: OANDAClient = None):
self.data_dir = Path(data_dir)
self.data_dir.mkdir(parents=True, exist_ok=True)
self.client = client or OANDAClient('demo', 'demo')
def _get_file_path(self, pair: str, granularity: str) -> Path:
"""Generate file path for storing data."""
pair = pair.upper().replace('/', '_')
return self.data_dir / f"{pair}_{granularity}.csv"
def download_data(
self,
pair: str,
granularity: str = 'H1',
count: int = 5000
) -> pd.DataFrame:
"""
Download historical data from OANDA.
Args:
pair: Currency pair
granularity: Timeframe
count: Number of candles
Returns:
Downloaded data as DataFrame
"""
print(f"Downloading {pair} {granularity} data...")
df = self.client.get_candles(pair, granularity, count)
# Save to file
file_path = self._get_file_path(pair, granularity)
df.to_csv(file_path)
print(f"Saved {len(df)} candles to {file_path}")
return df
def load_data(self, pair: str, granularity: str = 'H1') -> pd.DataFrame:
"""
Load data from local storage.
Args:
pair: Currency pair
granularity: Timeframe
Returns:
DataFrame or None if not found
"""
file_path = self._get_file_path(pair, granularity)
if file_path.exists():
df = pd.read_csv(file_path, index_col=0, parse_dates=True)
return df
else:
print(f"Data not found: {file_path}")
return None
def update_data(self, pair: str, granularity: str = 'H1') -> pd.DataFrame:
"""
Update existing data with latest candles.
Args:
pair: Currency pair
granularity: Timeframe
Returns:
Updated DataFrame
"""
existing = self.load_data(pair, granularity)
if existing is not None:
# Get new data from last timestamp
new_data = self.client.get_candles(pair, granularity, 100)
# Combine and remove duplicates
combined = pd.concat([existing, new_data])
combined = combined[~combined.index.duplicated(keep='last')]
combined = combined.sort_index()
# Save
file_path = self._get_file_path(pair, granularity)
combined.to_csv(file_path)
return combined
else:
return self.download_data(pair, granularity)
def get_available_data(self) -> List[dict]:
"""
List all available local data.
Returns:
List of available datasets
"""
available = []
for file_path in self.data_dir.glob('*.csv'):
parts = file_path.stem.split('_')
if len(parts) >= 3:
pair = f"{parts[0]}_{parts[1]}"
granularity = parts[2]
# Get file info
df = pd.read_csv(file_path, index_col=0, parse_dates=True)
available.append({
'pair': pair,
'granularity': granularity,
'candles': len(df),
'start': df.index[0].strftime('%Y-%m-%d'),
'end': df.index[-1].strftime('%Y-%m-%d'),
'file': file_path.name
})
return available
def validate_data(self, df: pd.DataFrame) -> dict:
"""
Validate data quality.
Args:
df: DataFrame to validate
Returns:
Validation report
"""
issues = []
# Check for missing values
missing = df.isnull().sum()
if missing.any():
issues.append(f"Missing values: {missing.to_dict()}")
# Check OHLC relationships
invalid_hl = df[df['high'] < df['low']]
if len(invalid_hl) > 0:
issues.append(f"Invalid H/L: {len(invalid_hl)} rows")
invalid_oh = df[(df['open'] > df['high']) | (df['open'] < df['low'])]
if len(invalid_oh) > 0:
issues.append(f"Open outside H/L: {len(invalid_oh)} rows")
invalid_ch = df[(df['close'] > df['high']) | (df['close'] < df['low'])]
if len(invalid_ch) > 0:
issues.append(f"Close outside H/L: {len(invalid_ch)} rows")
# Check for gaps (more than expected frequency)
if len(df) > 1:
time_diffs = df.index.to_series().diff().dropna()
median_diff = time_diffs.median()
large_gaps = time_diffs[time_diffs > median_diff * 5]
if len(large_gaps) > 0:
issues.append(f"Large gaps: {len(large_gaps)} (expected for weekends)")
return {
'valid': len(issues) == 0,
'rows': len(df),
'date_range': f"{df.index[0]} to {df.index[-1]}",
'issues': issues if issues else ['No issues found']
}
# Demonstrate data manager
manager = ForexDataManager('./demo_data')
# Download sample data
eur_usd = manager.download_data('EUR_USD', 'H1', 500)
print(f"\nDownloaded data shape: {eur_usd.shape}")
print(eur_usd.tail())
# Validate the downloaded data
validation = manager.validate_data(eur_usd)
print("\nData Validation Report")
print("=" * 50)
for k, v in validation.items():
print(f"{k}: {v}")
Exercise 4.2: Build Data Pipeline (Guided)
Complete the function to build a data download pipeline for multiple pairs and timeframes.
Solution 4.2
def build_data_pipeline(
pairs: List[str],
timeframes: List[str],
data_dir: str = './pipeline_data'
) -> dict:
"""
Build a data pipeline for multiple pairs and timeframes.
"""
manager = ForexDataManager(data_dir)
downloaded = []
failed = []
total_candles = 0
# Iterate through all combinations
for pair in pairs: # Iterate through pairs
for tf in timeframes: # Iterate through timeframes
try:
# Download data
df = manager.download_data(pair, tf, 500) # Call download method
downloaded.append({
'pair': pair,
'timeframe': tf,
'candles': len(df)
})
total_candles += len(df)
except Exception as e:
failed.append({
'pair': pair,
'timeframe': tf,
'error': str(e)
})
return {
'total_downloads': len(downloaded),
'total_candles': total_candles,
'failed': len(failed),
'downloaded': downloaded,
'failures': failed
}
4.4 Real-Time Data
Streaming Price Data
For live trading, you need real-time streaming prices. OANDA provides a streaming endpoint that pushes price updates as they occur.
class MockPriceStream:
"""
Simulates a real-time price stream.
In production, this would connect to OANDA's streaming API.
"""
def __init__(self, instruments: List[str], seed: int = 42):
self.instruments = [i.upper().replace('/', '_') for i in instruments]
self.rng = np.random.default_rng(seed)
self._provider = MockForexDataProvider(seed)
self._running = False
self._prices = {} # Current prices
# Initialize prices
for inst in self.instruments:
self._prices[inst] = self._provider.get_current_price(inst)
def _update_prices(self):
"""Simulate price updates."""
for inst in self.instruments:
current = self._prices[inst]
pip_size = 0.01 if 'JPY' in inst else 0.0001
# Random walk
change = self.rng.normal(0, 2) * pip_size
new_mid = current['mid'] + change
half_spread = (current['spread_pips'] / 2) * pip_size
self._prices[inst] = {
'pair': inst,
'bid': round(new_mid - half_spread, 5),
'ask': round(new_mid + half_spread, 5),
'mid': round(new_mid, 5),
'spread_pips': current['spread_pips'],
'timestamp': datetime.now(timezone.utc).isoformat()
}
def stream(self, duration_seconds: int = 10) -> Generator[dict, None, None]:
"""
Stream price updates.
Args:
duration_seconds: How long to stream
Yields:
Price update dictionaries
"""
self._running = True
start_time = time.time()
while self._running and (time.time() - start_time) < duration_seconds:
self._update_prices()
# Yield random instrument update
inst = self.rng.choice(self.instruments)
yield self._prices[inst]
# Simulate network delay
time.sleep(0.1)
def stop(self):
"""Stop the stream."""
self._running = False
def get_current_prices(self) -> Dict[str, dict]:
"""Get current prices for all instruments."""
return self._prices.copy()
# Demonstrate streaming
stream = MockPriceStream(['EUR_USD', 'GBP_USD', 'USD_JPY'])
print("Simulated Price Stream (5 updates)")
print("=" * 70)
count = 0
for price in stream.stream(duration_seconds=2):
print(f"[{price['timestamp'][-12:-1]}] {price['pair']:10} "
f"Bid: {price['bid']:.5f} | Ask: {price['ask']:.5f}")
count += 1
if count >= 5:
stream.stop()
break
class RealTimeDataHandler:
"""
Handles real-time forex data processing.
Attributes:
stream: Price stream source
buffer_size: Number of ticks to buffer
"""
def __init__(self, instruments: List[str], buffer_size: int = 100):
self.instruments = instruments
self.buffer_size = buffer_size
self.stream = MockPriceStream(instruments)
# Initialize buffers for each instrument
self.tick_buffers = {inst: [] for inst in instruments}
self.callbacks = [] # List of callback functions
def add_callback(self, callback):
"""Add a callback function to be called on each tick."""
self.callbacks.append(callback)
def _process_tick(self, tick: dict):
"""Process a single tick."""
pair = tick['pair']
# Add to buffer
if pair in self.tick_buffers:
self.tick_buffers[pair].append(tick)
# Trim buffer if too large
if len(self.tick_buffers[pair]) > self.buffer_size:
self.tick_buffers[pair] = self.tick_buffers[pair][-self.buffer_size:]
# Call callbacks
for callback in self.callbacks:
callback(tick)
def run(self, duration: int = 10):
"""Run the data handler."""
print(f"Starting data handler for {duration} seconds...")
for tick in self.stream.stream(duration):
self._process_tick(tick)
print("Data handler stopped.")
def get_tick_data(self, instrument: str) -> pd.DataFrame:
"""Get buffered tick data as DataFrame."""
inst = instrument.upper().replace('/', '_')
if inst not in self.tick_buffers:
return pd.DataFrame()
ticks = self.tick_buffers[inst]
if not ticks:
return pd.DataFrame()
df = pd.DataFrame(ticks)
df['timestamp'] = pd.to_datetime(df['timestamp'])
df = df.set_index('timestamp')
return df
def get_latest_prices(self) -> Dict[str, dict]:
"""Get latest price for each instrument."""
latest = {}
for inst, buffer in self.tick_buffers.items():
if buffer:
latest[inst] = buffer[-1]
return latest
# Demonstrate real-time handler
handler = RealTimeDataHandler(['EUR_USD', 'GBP_USD'], buffer_size=50)
# Add a simple callback
tick_count = [0]
def count_ticks(tick):
tick_count[0] += 1
handler.add_callback(count_ticks)
# Run for short duration
handler.run(duration=2)
print(f"\nTotal ticks received: {tick_count[0]}")
print(f"\nLatest Prices:")
for pair, price in handler.get_latest_prices().items():
print(f" {pair}: {price['mid']:.5f}")
Exercise 4.3: Real-Time Feed (Guided)
Complete the function to process real-time price updates and calculate simple metrics.
Solution 4.3
def process_price_stream(
instruments: List[str],
duration: int = 5,
alert_threshold_pips: float = 3.0
) -> dict:
"""
Process a price stream and generate statistics.
"""
stream = MockPriceStream(instruments)
stats = {}
alerts = []
first_prices = {}
for tick in stream.stream(duration):
pair = tick['pair']
mid = tick['mid']
pip_size = 0.01 if 'JPY' in pair else 0.0001
# Initialize stats for pair
if pair not in stats:
stats[pair] = {
'ticks': 0,
'prices': [],
'first': mid,
'last': mid
}
first_prices[pair] = mid
# Update stats
stats[pair]['ticks'] += 1
stats[pair]['prices'].append(mid) # Append price to list
stats[pair]['last'] = mid
# Check for significant move
move_pips = abs(mid - first_prices[pair]) / pip_size # Convert to pips
if move_pips >= alert_threshold_pips:
direction = 'UP' if mid > first_prices[pair] else 'DOWN' # Determine direction
alerts.append({
'pair': pair,
'move_pips': round(move_pips, 1),
'direction': direction,
'timestamp': tick['timestamp']
})
# Calculate final statistics
summary = {}
for pair, data in stats.items():
pip_size = 0.01 if 'JPY' in pair else 0.0001
prices = data['prices']
summary[pair] = {
'tick_count': data['ticks'],
'high': max(prices) if prices else 0,
'low': min(prices) if prices else 0,
'range_pips': round((max(prices) - min(prices)) / pip_size, 1) if prices else 0,
'net_change_pips': round((data['last'] - data['first']) / pip_size, 1)
}
return {
'duration': duration,
'instruments': instruments,
'summary': summary,
'alerts': alerts
}
Exercise 4.4: Data Source Manager (Open-ended)
Build a multi-source data manager that can fetch data from different providers.
Your implementation:
Solution 4.4
class MultiSourceDataManager:
"""
Manages multiple forex data sources with fallback.
"""
def __init__(self, cache_dir: str = './cache'):
self.sources = {} # name -> {provider, priority, status}
self.cache_dir = Path(cache_dir)
self.cache_dir.mkdir(parents=True, exist_ok=True)
self.cache = {} # (pair, timeframe) -> (data, timestamp)
self.cache_ttl = 3600 # 1 hour cache
def add_source(self, name: str, provider, priority: int = 1):
"""Add a data source with priority (lower = higher priority)."""
self.sources[name] = {
'provider': provider,
'priority': priority,
'status': 'active',
'failures': 0
}
def _get_sorted_sources(self) -> List[Tuple[str, dict]]:
"""Get sources sorted by priority."""
active = [(n, s) for n, s in self.sources.items() if s['status'] == 'active']
return sorted(active, key=lambda x: x[1]['priority'])
def _check_cache(self, pair: str, timeframe: str) -> Optional[pd.DataFrame]:
"""Check if data is in cache and not expired."""
key = (pair, timeframe)
if key in self.cache:
data, timestamp = self.cache[key]
if time.time() - timestamp < self.cache_ttl:
return data
return None
def _update_cache(self, pair: str, timeframe: str, data: pd.DataFrame):
"""Update cache with new data."""
self.cache[(pair, timeframe)] = (data, time.time())
def fetch_data(self, pair: str, timeframe: str, source_name: str = None) -> pd.DataFrame:
"""Fetch data from a specific source."""
if source_name is None:
sources = self._get_sorted_sources()
source_name = sources[0][0] if sources else None
if source_name not in self.sources:
raise ValueError(f"Source {source_name} not found")
source = self.sources[source_name]
provider = source['provider']
try:
data = provider.get_candles(pair, timeframe, 100)
source['failures'] = 0
self._update_cache(pair, timeframe, data)
return data
except Exception as e:
source['failures'] += 1
if source['failures'] >= 3:
source['status'] = 'disabled'
raise
def get_with_fallback(self, pair: str, timeframe: str) -> pd.DataFrame:
"""Get data with automatic fallback."""
# Check cache first
cached = self._check_cache(pair, timeframe)
if cached is not None:
return cached
# Try sources in priority order
for name, source in self._get_sorted_sources():
try:
return self.fetch_data(pair, timeframe, name)
except Exception as e:
print(f"Source {name} failed: {e}")
continue
raise Exception("All sources failed")
def validate_source(self, name: str) -> dict:
"""Validate a data source."""
if name not in self.sources:
return {'valid': False, 'error': 'Source not found'}
source = self.sources[name]
try:
data = source['provider'].get_candles('EUR_USD', 'H1', 10)
return {
'valid': True,
'name': name,
'status': source['status'],
'sample_rows': len(data),
'failures': source['failures']
}
except Exception as e:
return {'valid': False, 'error': str(e)}
# Test
manager = MultiSourceDataManager()
# Add sources
manager.add_source('oanda', OANDAClient('demo', 'demo'), priority=1)
manager.add_source('backup', OANDAClient('demo', 'demo'), priority=2)
# Fetch with fallback
data = manager.get_with_fallback('EUR_USD', 'H1')
print(f"Fetched {len(data)} rows")
# Validate sources
for name in manager.sources:
print(f"\n{name}: {manager.validate_source(name)}")
Exercise 4.5: Streaming Data Aggregator (Open-ended)
Build a streaming data aggregator that converts tick data to OHLC bars.
Your implementation:
Solution 4.5
class StreamingAggregator:
"""
Aggregates tick data into OHLC bars.
"""
TIMEFRAME_SECONDS = {
'M1': 60, 'M5': 300, 'M15': 900, 'M30': 1800,
'H1': 3600, 'H4': 14400, 'D': 86400
}
def __init__(self):
self.timeframes = {} # timeframe -> {bars, current_bar, callback}
def add_timeframe(self, timeframe: str, callback: callable = None):
"""Add a timeframe to aggregate."""
self.timeframes[timeframe] = {
'seconds': self.TIMEFRAME_SECONDS.get(timeframe, 3600),
'bars': [],
'current_bar': None,
'callback': callback
}
def _get_bar_timestamp(self, tick_time: datetime, seconds: int) -> datetime:
"""Get the bar start timestamp for a tick."""
epoch = tick_time.timestamp()
bar_epoch = (epoch // seconds) * seconds
return datetime.fromtimestamp(bar_epoch, tz=timezone.utc)
def process_tick(self, tick: dict):
"""Process a single tick."""
tick_time = datetime.fromisoformat(tick['timestamp'].replace('Z', '+00:00'))
mid = tick['mid']
for tf, data in self.timeframes.items():
bar_time = self._get_bar_timestamp(tick_time, data['seconds'])
# Check if new bar needed
if data['current_bar'] is None or data['current_bar']['timestamp'] != bar_time:
# Complete previous bar
if data['current_bar'] is not None:
data['bars'].append(data['current_bar'])
if data['callback']:
data['callback'](tf, data['current_bar'])
# Start new bar
data['current_bar'] = {
'timestamp': bar_time,
'open': mid,
'high': mid,
'low': mid,
'close': mid,
'tick_count': 1
}
else:
# Update current bar
bar = data['current_bar']
bar['high'] = max(bar['high'], mid)
bar['low'] = min(bar['low'], mid)
bar['close'] = mid
bar['tick_count'] += 1
def get_current_bar(self, timeframe: str) -> dict:
"""Get the current (incomplete) bar."""
if timeframe not in self.timeframes:
return None
return self.timeframes[timeframe]['current_bar']
def get_completed_bars(self, timeframe: str) -> List[dict]:
"""Get all completed bars."""
if timeframe not in self.timeframes:
return []
return self.timeframes[timeframe]['bars']
def to_dataframe(self, timeframe: str) -> pd.DataFrame:
"""Convert completed bars to DataFrame."""
bars = self.get_completed_bars(timeframe)
if not bars:
return pd.DataFrame()
df = pd.DataFrame(bars)
df = df.set_index('timestamp')
return df
# Test aggregator
aggregator = StreamingAggregator()
# Add callback
def on_bar_complete(tf, bar):
print(f"New {tf} bar: O={bar['open']:.5f} H={bar['high']:.5f} "
f"L={bar['low']:.5f} C={bar['close']:.5f}")
aggregator.add_timeframe('M1', callback=on_bar_complete)
# Process some ticks
stream = MockPriceStream(['EUR_USD'])
for tick in stream.stream(duration=5):
aggregator.process_tick(tick)
print(f"\nCompleted bars: {len(aggregator.get_completed_bars('M1'))}")
print(f"Current bar: {aggregator.get_current_bar('M1')}")
Exercise 4.6: Forex Data System (Open-ended)
Build a complete forex data system that combines all the components from this module.
Your implementation:
Solution 4.6
class ForexDataSystem:
"""
Complete forex data management system.
"""
def __init__(self, data_dir: str = './forex_system'):
self.data_dir = Path(data_dir)
self.data_dir.mkdir(parents=True, exist_ok=True)
self.client = OANDAClient('demo', 'demo')
self.data_manager = ForexDataManager(str(self.data_dir))
self.stream = None
self.aggregator = StreamingAggregator()
self.instruments = []
self.timeframes = []
self.is_streaming = False
self.latest_prices = {}
def initialize(self, instruments: List[str], timeframes: List[str]):
"""Initialize the system."""
self.instruments = [i.upper().replace('/', '_') for i in instruments]
self.timeframes = timeframes
# Set up aggregator
for tf in timeframes:
self.aggregator.add_timeframe(tf)
print(f"Initialized with {len(instruments)} instruments, {len(timeframes)} timeframes")
def download_history(self, start_date: datetime = None, end_date: datetime = None):
"""Download historical data."""
for pair in self.instruments:
for tf in self.timeframes:
print(f"Downloading {pair} {tf}...")
self.data_manager.download_data(pair, tf, 1000)
def start_streaming(self):
"""Start streaming prices."""
self.is_streaming = True
self.stream = MockPriceStream(self.instruments)
print("Streaming started")
def stop_streaming(self):
"""Stop streaming."""
self.is_streaming = False
if self.stream:
self.stream.stop()
print("Streaming stopped")
def process_ticks(self, duration: int = 5):
"""Process streaming ticks."""
if not self.is_streaming:
self.start_streaming()
for tick in self.stream.stream(duration):
self.aggregator.process_tick(tick)
self.latest_prices[tick['pair']] = tick
def get_data(self, instrument: str, timeframe: str) -> pd.DataFrame:
"""Get historical data."""
inst = instrument.upper().replace('/', '_')
return self.data_manager.load_data(inst, timeframe)
def get_latest_price(self, instrument: str) -> dict:
"""Get latest streaming price."""
inst = instrument.upper().replace('/', '_')
return self.latest_prices.get(inst)
def health_check(self) -> dict:
"""Check system health."""
return {
'status': 'healthy',
'instruments': len(self.instruments),
'timeframes': len(self.timeframes),
'streaming': self.is_streaming,
'latest_prices': len(self.latest_prices),
'data_files': len(list(self.data_dir.glob('*.csv')))
}
def summary(self):
"""Print system summary."""
print("\n" + "=" * 60)
print("FOREX DATA SYSTEM SUMMARY")
print("=" * 60)
health = self.health_check()
for k, v in health.items():
print(f" {k}: {v}")
print("=" * 60)
# Test the system
system = ForexDataSystem('./test_system')
system.initialize(
instruments=['EUR_USD', 'GBP_USD', 'USD_JPY'],
timeframes=['M1', 'H1']
)
# Download history
system.download_history()
# Process some streaming data
system.start_streaming()
system.process_ticks(duration=3)
system.stop_streaming()
# Print summary
system.summary()
# Get latest prices
print("\nLatest Prices:")
for pair in ['EUR_USD', 'GBP_USD']:
price = system.get_latest_price(pair)
if price:
print(f" {pair}: {price['mid']:.5f}")
Module Project: Forex Data System
Build a production-ready forex data system combining all concepts from this module.
class ProductionForexDataSystem:
"""
Production-ready forex data management system.
Features:
- Multi-source data fetching with fallback
- Historical data management with caching
- Real-time streaming with tick aggregation
- Data validation and quality monitoring
- Health checks and status reporting
"""
def __init__(self, config: dict = None):
self.config = config or self._default_config()
self.data_dir = Path(self.config['data_dir'])
self.data_dir.mkdir(parents=True, exist_ok=True)
# Components
self.client = OANDAClient(
self.config.get('account_id', 'demo'),
self.config.get('access_token', 'demo'),
self.config.get('environment', 'practice')
)
self.instruments = []
self.timeframes = []
self.historical_data = {} # (pair, tf) -> DataFrame
self.latest_prices = {}
self.stream = None
self.is_running = False
# Metrics
self.metrics = {
'ticks_received': 0,
'bars_completed': 0,
'errors': 0,
'last_update': None
}
def _default_config(self) -> dict:
return {
'data_dir': './forex_data_system',
'account_id': 'demo',
'access_token': 'demo',
'environment': 'practice',
'default_history_count': 1000,
'cache_ttl': 3600
}
# ==================== Initialization ====================
def initialize(
self,
instruments: List[str],
timeframes: List[str] = None
):
"""
Initialize the data system.
Args:
instruments: List of currency pairs
timeframes: List of timeframes (default: H1, H4, D)
"""
self.instruments = [i.upper().replace('/', '_') for i in instruments]
self.timeframes = timeframes or ['H1', 'H4', 'D']
print(f"Initializing Forex Data System")
print(f" Instruments: {len(self.instruments)}")
print(f" Timeframes: {self.timeframes}")
# ==================== Historical Data ====================
def download_historical(
self,
count: int = None,
instruments: List[str] = None,
timeframes: List[str] = None
):
"""
Download historical data for all or specified instruments.
"""
instruments = instruments or self.instruments
timeframes = timeframes or self.timeframes
count = count or self.config['default_history_count']
total = len(instruments) * len(timeframes)
completed = 0
for pair in instruments:
for tf in timeframes:
try:
df = self.client.get_candles(pair, tf, count)
self.historical_data[(pair, tf)] = df
# Save to file
file_path = self.data_dir / f"{pair}_{tf}.csv"
df.to_csv(file_path)
completed += 1
print(f"[{completed}/{total}] Downloaded {pair} {tf}: {len(df)} candles")
except Exception as e:
self.metrics['errors'] += 1
print(f"Error downloading {pair} {tf}: {e}")
print(f"\nDownload complete: {completed}/{total} successful")
def load_historical(self, pair: str, timeframe: str) -> pd.DataFrame:
"""
Load historical data (from memory or file).
"""
pair = pair.upper().replace('/', '_')
# Check memory cache
if (pair, timeframe) in self.historical_data:
return self.historical_data[(pair, timeframe)]
# Check file
file_path = self.data_dir / f"{pair}_{timeframe}.csv"
if file_path.exists():
df = pd.read_csv(file_path, index_col=0, parse_dates=True)
self.historical_data[(pair, timeframe)] = df
return df
return None
# ==================== Real-Time Data ====================
def start_streaming(self):
"""Start real-time price streaming."""
self.stream = MockPriceStream(self.instruments)
self.is_running = True
print("Streaming started for:", self.instruments)
def stop_streaming(self):
"""Stop streaming."""
self.is_running = False
if self.stream:
self.stream.stop()
print("Streaming stopped")
def process_stream(self, duration: int = 10, callback=None):
"""
Process streaming data for a duration.
Args:
duration: Seconds to stream
callback: Optional callback for each tick
"""
if not self.is_running:
self.start_streaming()
for tick in self.stream.stream(duration):
self.metrics['ticks_received'] += 1
self.latest_prices[tick['pair']] = tick
self.metrics['last_update'] = tick['timestamp']
if callback:
callback(tick)
def get_latest_price(self, pair: str) -> dict:
"""Get latest price for an instrument."""
pair = pair.upper().replace('/', '_')
return self.latest_prices.get(pair)
def get_all_prices(self) -> pd.DataFrame:
"""Get all latest prices as DataFrame."""
if not self.latest_prices:
return pd.DataFrame()
return pd.DataFrame(self.latest_prices.values())
# ==================== Data Validation ====================
def validate_data(self, pair: str, timeframe: str) -> dict:
"""Validate data quality."""
df = self.load_historical(pair, timeframe)
if df is None:
return {'valid': False, 'error': 'Data not found'}
issues = []
# Check for missing data
if df.isnull().any().any():
issues.append('Contains missing values')
# Check OHLC relationships
if not (df['high'] >= df['low']).all():
issues.append('High < Low in some rows')
# Check for large gaps
if len(df) > 1:
gaps = df.index.to_series().diff().dropna()
median_gap = gaps.median()
large_gaps = (gaps > median_gap * 10).sum()
if large_gaps > 0:
issues.append(f'{large_gaps} large gaps (weekends expected)')
return {
'valid': len(issues) == 0,
'pair': pair,
'timeframe': timeframe,
'rows': len(df),
'date_range': f"{df.index[0]} to {df.index[-1]}",
'issues': issues if issues else ['No issues']
}
# ==================== Health & Status ====================
def health_check(self) -> dict:
"""Perform system health check."""
data_files = list(self.data_dir.glob('*.csv'))
return {
'status': 'healthy' if self.metrics['errors'] < 10 else 'degraded',
'streaming': self.is_running,
'instruments': len(self.instruments),
'timeframes': len(self.timeframes),
'data_files': len(data_files),
'cached_datasets': len(self.historical_data),
'latest_prices': len(self.latest_prices),
'ticks_received': self.metrics['ticks_received'],
'errors': self.metrics['errors'],
'last_update': self.metrics['last_update']
}
def print_status(self):
"""Print detailed system status."""
health = self.health_check()
print("\n" + "=" * 60)
print("FOREX DATA SYSTEM STATUS")
print("=" * 60)
print(f"\nSystem Health: {health['status'].upper()}")
print(f"Streaming: {'Active' if health['streaming'] else 'Stopped'}")
print(f"\nConfiguration:")
print(f" Instruments: {health['instruments']}")
print(f" Timeframes: {health['timeframes']}")
print(f"\nData:")
print(f" Data Files: {health['data_files']}")
print(f" Cached Datasets: {health['cached_datasets']}")
print(f" Latest Prices: {health['latest_prices']}")
print(f"\nMetrics:")
print(f" Ticks Received: {health['ticks_received']}")
print(f" Errors: {health['errors']}")
print(f" Last Update: {health['last_update']}")
if self.latest_prices:
print(f"\nLatest Prices:")
for pair, price in self.latest_prices.items():
print(f" {pair}: {price['mid']:.5f} (spread: {price['spread_pips']} pips)")
print("\n" + "=" * 60)
# Demonstrate the Production Forex Data System
# Create and configure system
system = ProductionForexDataSystem({
'data_dir': './production_forex_data',
'account_id': 'demo-account',
'access_token': 'demo-token',
'environment': 'practice',
'default_history_count': 500
})
# Initialize
system.initialize(
instruments=['EUR_USD', 'GBP_USD', 'USD_JPY', 'AUD_USD'],
timeframes=['H1', 'H4', 'D']
)
# Download historical data
system.download_historical(count=200)
# Validate data quality
print("\nData Validation Results")
print("=" * 50)
for pair in ['EUR_USD', 'GBP_USD']:
validation = system.validate_data(pair, 'H1')
print(f"\n{pair} H1:")
print(f" Valid: {validation['valid']}")
print(f" Rows: {validation['rows']}")
print(f" Issues: {validation['issues']}")
# Start streaming and process for a few seconds
print("\nProcessing live stream...")
system.start_streaming()
# Process with a simple callback
def print_tick(tick):
print(f" [{tick['timestamp'][-12:-1]}] {tick['pair']}: {tick['mid']:.5f}")
system.process_stream(duration=2, callback=print_tick)
system.stop_streaming()
# Print final status
system.print_status()
Key Takeaways
- Multiple data sources are available for forex: OANDA, FXCM, Alpha Vantage, Yahoo Finance
- OANDA API provides free access to historical and real-time forex data via REST and streaming endpoints
- Historical data management requires proper storage, validation, and update mechanisms
- Real-time streaming enables live trading but requires tick aggregation for bar-based analysis
- Data validation is critical - check for gaps, invalid OHLC relationships, and missing values
- Production systems need health checks, error handling, and fallback data sources
- Caching reduces API calls and improves performance for frequently accessed data
Next: Part 2 - Analysis & Strategies
Module 5: Technical Analysis for Forex & Futures
Part 2: Analysis & Strategies
| Duration | Exercises |
|---|---|
| ~2.5 hours | 6 |
Learning Objectives
- Identify and trade chart patterns in forex markets
- Build forex-specific indicators like currency strength meters
- Apply multi-timeframe analysis for better entries
- Trade using pure price action without indicators
Prerequisites
- Modules 1-4 (Forex/Futures fundamentals, data tools)
- Basic understanding of candlestick charts
- Python pandas and numpy proficiency
import pandas as pd
import numpy as np
from datetime import datetime, timedelta
from dataclasses import dataclass, field
from typing import List, Dict, Tuple, Optional
from enum import Enum
import warnings
warnings.filterwarnings('ignore')
5.1 Forex Chart Patterns
Chart patterns are visual formations that help identify potential price movements. In forex, these patterns are particularly reliable due to high liquidity.
Support and Resistance Levels
Support and resistance are price levels where buying or selling pressure historically reverses price direction.
class SupportResistanceFinder:
"""Identifies support and resistance levels from price data."""
def __init__(self, lookback: int = 20, touch_threshold: float = 0.001):
self.lookback = lookback
self.touch_threshold = touch_threshold
def find_pivot_points(self, df: pd.DataFrame) -> Dict[str, List[float]]:
"""Find swing highs and lows as potential S/R levels."""
highs = []
lows = []
for i in range(self.lookback, len(df) - self.lookback):
window_high = df['high'].iloc[i-self.lookback:i+self.lookback+1]
if df['high'].iloc[i] == window_high.max():
highs.append(df['high'].iloc[i])
window_low = df['low'].iloc[i-self.lookback:i+self.lookback+1]
if df['low'].iloc[i] == window_low.min():
lows.append(df['low'].iloc[i])
return {'resistance': highs, 'support': lows}
def cluster_levels(self, levels: List[float], tolerance: float = 0.002) -> List[float]:
"""Cluster nearby levels into single stronger levels."""
if not levels:
return []
sorted_levels = sorted(levels)
clusters = [[sorted_levels[0]]]
for level in sorted_levels[1:]:
if (level - clusters[-1][-1]) / clusters[-1][-1] < tolerance:
clusters[-1].append(level)
else:
clusters.append([level])
return [np.mean(cluster) for cluster in clusters]
def count_touches(self, df: pd.DataFrame, level: float) -> int:
"""Count how many times price touched a level."""
touches = 0
for _, row in df.iterrows():
if abs(row['high'] - level) / level < self.touch_threshold:
touches += 1
elif abs(row['low'] - level) / level < self.touch_threshold:
touches += 1
return touches
def get_key_levels(self, df: pd.DataFrame, min_touches: int = 2) -> Dict[str, List[Dict]]:
"""Get key S/R levels with touch counts."""
pivots = self.find_pivot_points(df)
resistance_clustered = self.cluster_levels(pivots['resistance'])
support_clustered = self.cluster_levels(pivots['support'])
key_levels = {'resistance': [], 'support': []}
for level in resistance_clustered:
touches = self.count_touches(df, level)
if touches >= min_touches:
key_levels['resistance'].append({
'level': level,
'touches': touches,
'strength': 'strong' if touches >= 4 else 'moderate'
})
for level in support_clustered:
touches = self.count_touches(df, level)
if touches >= min_touches:
key_levels['support'].append({
'level': level,
'touches': touches,
'strength': 'strong' if touches >= 4 else 'moderate'
})
return key_levels
# Generate demo data
np.random.seed(42)
dates = pd.date_range('2024-01-01', periods=200, freq='4h')
base_price = 1.0850
prices = [base_price]
for _ in range(199):
change = np.random.normal(0, 0.002)
if prices[-1] > 1.0950:
change -= 0.001
elif prices[-1] < 1.0750:
change += 0.001
prices.append(prices[-1] + change)
demo_df = pd.DataFrame({
'timestamp': dates,
'open': prices,
'high': [p + np.random.uniform(0.001, 0.003) for p in prices],
'low': [p - np.random.uniform(0.001, 0.003) for p in prices],
'close': [p + np.random.normal(0, 0.001) for p in prices]
})
sr_finder = SupportResistanceFinder(lookback=10)
key_levels = sr_finder.get_key_levels(demo_df)
print("Key Support Levels:")
for level in key_levels['support'][:3]:
print(f" {level['level']:.5f} - {level['touches']} touches ({level['strength']})")
print("\nKey Resistance Levels:")
for level in key_levels['resistance'][:3]:
print(f" {level['level']:.5f} - {level['touches']} touches ({level['strength']})")
Trend Line Detection
Trend lines connect swing highs (downtrend) or swing lows (uptrend) to identify trend direction and potential breakout points.
class TrendLineDetector:
"""Automatically detect and validate trend lines."""
def __init__(self, min_touches: int = 3, tolerance: float = 0.001):
self.min_touches = min_touches
self.tolerance = tolerance
def find_swing_points(self, df: pd.DataFrame, lookback: int = 5) -> Dict[str, pd.DataFrame]:
"""Identify swing highs and lows."""
swing_highs = []
swing_lows = []
for i in range(lookback, len(df) - lookback):
if df['high'].iloc[i] == df['high'].iloc[i-lookback:i+lookback+1].max():
swing_highs.append({'index': i, 'price': df['high'].iloc[i]})
if df['low'].iloc[i] == df['low'].iloc[i-lookback:i+lookback+1].min():
swing_lows.append({'index': i, 'price': df['low'].iloc[i]})
return {
'highs': pd.DataFrame(swing_highs),
'lows': pd.DataFrame(swing_lows)
}
def fit_trend_line(self, points: pd.DataFrame) -> Optional[Dict]:
"""Fit a trend line through points using linear regression."""
if len(points) < 2:
return None
x = points['index'].values
y = points['price'].values
slope = np.polyfit(x, y, 1)[0]
intercept = np.mean(y) - slope * np.mean(x)
y_pred = slope * x + intercept
ss_res = np.sum((y - y_pred) ** 2)
ss_tot = np.sum((y - np.mean(y)) ** 2)
r_squared = 1 - (ss_res / ss_tot) if ss_tot != 0 else 0
return {
'slope': slope,
'intercept': intercept,
'r_squared': r_squared,
'points_used': len(points),
'direction': 'up' if slope > 0 else 'down'
}
def detect_trend_lines(self, df: pd.DataFrame) -> Dict[str, List[Dict]]:
"""Detect both uptrend and downtrend lines."""
swings = self.find_swing_points(df)
trend_lines = {'uptrend': [], 'downtrend': []}
if len(swings['lows']) >= self.min_touches:
recent_lows = swings['lows'].tail(10)
trendline = self.fit_trend_line(recent_lows)
if trendline and trendline['slope'] > 0 and trendline['r_squared'] > 0.7:
trend_lines['uptrend'].append(trendline)
if len(swings['highs']) >= self.min_touches:
recent_highs = swings['highs'].tail(10)
trendline = self.fit_trend_line(recent_highs)
if trendline and trendline['slope'] < 0 and trendline['r_squared'] > 0.7:
trend_lines['downtrend'].append(trendline)
return trend_lines
# Demo
detector = TrendLineDetector()
trend_lines = detector.detect_trend_lines(demo_df)
print("Detected Trend Lines:")
for direction, lines in trend_lines.items():
for line in lines:
print(f" {direction.upper()}: slope={line['slope']:.6f}, R²={line['r_squared']:.3f}")
Chart Pattern Recognition
Common patterns include head & shoulders, double tops/bottoms, triangles, and flags.
class ChartPatternRecognizer:
"""Recognize common chart patterns in price data."""
def __init__(self, tolerance: float = 0.005):
self.tolerance = tolerance
def find_double_top(self, df: pd.DataFrame, lookback: int = 50) -> Optional[Dict]:
"""Detect double top pattern (bearish reversal)."""
recent = df.tail(lookback)
highs = recent['high'].values
peak_indices = []
for i in range(5, len(highs) - 5):
if highs[i] == max(highs[i-5:i+6]):
peak_indices.append(i)
if len(peak_indices) >= 2:
peak1_idx, peak2_idx = peak_indices[-2], peak_indices[-1]
peak1, peak2 = highs[peak1_idx], highs[peak2_idx]
if abs(peak1 - peak2) / peak1 < self.tolerance:
neckline = min(recent['low'].iloc[peak1_idx:peak2_idx])
return {
'pattern': 'double_top',
'signal': 'bearish',
'peak1': peak1,
'peak2': peak2,
'neckline': neckline,
'target': neckline - (peak1 - neckline)
}
return None
def find_double_bottom(self, df: pd.DataFrame, lookback: int = 50) -> Optional[Dict]:
"""Detect double bottom pattern (bullish reversal)."""
recent = df.tail(lookback)
lows = recent['low'].values
trough_indices = []
for i in range(5, len(lows) - 5):
if lows[i] == min(lows[i-5:i+6]):
trough_indices.append(i)
if len(trough_indices) >= 2:
trough1_idx, trough2_idx = trough_indices[-2], trough_indices[-1]
trough1, trough2 = lows[trough1_idx], lows[trough2_idx]
if abs(trough1 - trough2) / trough1 < self.tolerance:
neckline = max(recent['high'].iloc[trough1_idx:trough2_idx])
return {
'pattern': 'double_bottom',
'signal': 'bullish',
'trough1': trough1,
'trough2': trough2,
'neckline': neckline,
'target': neckline + (neckline - trough1)
}
return None
def find_triangle(self, df: pd.DataFrame, lookback: int = 40) -> Optional[Dict]:
"""Detect triangle patterns."""
recent = df.tail(lookback)
x = np.arange(len(recent))
high_slope = np.polyfit(x, recent['high'].values, 1)[0]
low_slope = np.polyfit(x, recent['low'].values, 1)[0]
if high_slope < -0.0001 and low_slope > 0.0001:
pattern_type = 'symmetrical'
signal = 'neutral'
elif abs(high_slope) < 0.0001 and low_slope > 0.0001:
pattern_type = 'ascending'
signal = 'bullish'
elif high_slope < -0.0001 and abs(low_slope) < 0.0001:
pattern_type = 'descending'
signal = 'bearish'
else:
return None
return {
'pattern': f'{pattern_type}_triangle',
'signal': signal,
'high_slope': high_slope,
'low_slope': low_slope
}
def scan_patterns(self, df: pd.DataFrame) -> List[Dict]:
"""Scan for all patterns."""
patterns = []
if (dt := self.find_double_top(df)):
patterns.append(dt)
if (db := self.find_double_bottom(df)):
patterns.append(db)
if (tri := self.find_triangle(df)):
patterns.append(tri)
return patterns
# Demo
pattern_recognizer = ChartPatternRecognizer()
patterns = pattern_recognizer.scan_patterns(demo_df)
print("Detected Patterns:")
for p in patterns:
print(f" {p['pattern']}: {p['signal']} signal")
5.2 Forex-Specific Indicators
Forex markets have unique indicators not commonly used in equity markets.
Currency Strength Meter
Measures the relative strength of individual currencies across multiple pairs.
class CurrencyStrengthMeter:
"""Calculate relative strength of currencies."""
MAJOR_CURRENCIES = ['USD', 'EUR', 'GBP', 'JPY', 'AUD', 'CAD', 'CHF', 'NZD']
PAIR_MAPPING = {
'EURUSD': ('EUR', 'USD'),
'GBPUSD': ('GBP', 'USD'),
'USDJPY': ('USD', 'JPY'),
'AUDUSD': ('AUD', 'USD'),
'USDCAD': ('USD', 'CAD'),
'USDCHF': ('USD', 'CHF'),
'NZDUSD': ('NZD', 'USD'),
'EURGBP': ('EUR', 'GBP'),
'EURJPY': ('EUR', 'JPY'),
'GBPJPY': ('GBP', 'JPY'),
}
def __init__(self, period: int = 14):
self.period = period
def calculate_pair_change(self, prices: pd.Series) -> float:
"""Calculate percentage change over period."""
if len(prices) < self.period:
return 0.0
return (prices.iloc[-1] / prices.iloc[-self.period] - 1) * 100
def calculate_strength(self, pair_data: Dict[str, pd.Series]) -> Dict[str, float]:
"""Calculate strength score for each currency."""
strength = {currency: 0.0 for currency in self.MAJOR_CURRENCIES}
count = {currency: 0 for currency in self.MAJOR_CURRENCIES}
for pair, prices in pair_data.items():
if pair not in self.PAIR_MAPPING:
continue
base, quote = self.PAIR_MAPPING[pair]
change = self.calculate_pair_change(prices)
strength[base] += change
count[base] += 1
strength[quote] -= change
count[quote] += 1
for currency in self.MAJOR_CURRENCIES:
if count[currency] > 0:
strength[currency] /= count[currency]
return strength
def get_rankings(self, strength: Dict[str, float]) -> List[Tuple[str, float]]:
"""Rank currencies from strongest to weakest."""
return sorted(strength.items(), key=lambda x: x[1], reverse=True)
def get_best_pairs(self, strength: Dict[str, float], top_n: int = 3) -> List[Dict]:
"""Find best pairs to trade (strong vs weak currencies)."""
rankings = self.get_rankings(strength)
strongest = [r[0] for r in rankings[:top_n]]
weakest = [r[0] for r in rankings[-top_n:]]
opportunities = []
for strong in strongest:
for weak in weakest:
for pair, (base, quote) in self.PAIR_MAPPING.items():
if base == strong and quote == weak:
opportunities.append({
'pair': pair,
'direction': 'LONG',
'strength_diff': strength[strong] - strength[weak]
})
elif base == weak and quote == strong:
opportunities.append({
'pair': pair,
'direction': 'SHORT',
'strength_diff': strength[strong] - strength[weak]
})
return sorted(opportunities, key=lambda x: x['strength_diff'], reverse=True)
# Demo
np.random.seed(42)
pair_data = {}
for pair in CurrencyStrengthMeter.PAIR_MAPPING.keys():
base_price = 1.0 if 'JPY' not in pair else 110.0
prices = [base_price]
for _ in range(50):
prices.append(prices[-1] * (1 + np.random.normal(0, 0.005)))
pair_data[pair] = pd.Series(prices)
csm = CurrencyStrengthMeter(period=14)
strength = csm.calculate_strength(pair_data)
rankings = csm.get_rankings(strength)
print("Currency Strength Rankings:")
for currency, score in rankings:
bar = '+' * int(abs(score) * 10) if score > 0 else '-' * int(abs(score) * 10)
print(f" {currency}: {score:+.3f}% {bar}")
print("\nBest Trading Opportunities:")
for opp in csm.get_best_pairs(strength)[:3]:
print(f" {opp['pair']} {opp['direction']}: strength diff = {opp['strength_diff']:.3f}%")
Pivot Points
Pivot points are significant levels calculated from prior period's high, low, and close. Very popular in forex day trading.
class PivotPointCalculator:
"""Calculate various pivot point types."""
def standard_pivots(self, high: float, low: float, close: float) -> Dict[str, float]:
"""Calculate standard (floor) pivot points."""
pp = (high + low + close) / 3
return {
'R3': high + 2 * (pp - low),
'R2': pp + (high - low),
'R1': 2 * pp - low,
'PP': pp,
'S1': 2 * pp - high,
'S2': pp - (high - low),
'S3': low - 2 * (high - pp)
}
def fibonacci_pivots(self, high: float, low: float, close: float) -> Dict[str, float]:
"""Calculate Fibonacci pivot points."""
pp = (high + low + close) / 3
range_hl = high - low
return {
'R3': pp + range_hl * 1.000,
'R2': pp + range_hl * 0.618,
'R1': pp + range_hl * 0.382,
'PP': pp,
'S1': pp - range_hl * 0.382,
'S2': pp - range_hl * 0.618,
'S3': pp - range_hl * 1.000
}
def camarilla_pivots(self, high: float, low: float, close: float) -> Dict[str, float]:
"""Calculate Camarilla pivot points."""
range_hl = high - low
return {
'R4': close + range_hl * 1.1 / 2,
'R3': close + range_hl * 1.1 / 4,
'R2': close + range_hl * 1.1 / 6,
'R1': close + range_hl * 1.1 / 12,
'PP': (high + low + close) / 3,
'S1': close - range_hl * 1.1 / 12,
'S2': close - range_hl * 1.1 / 6,
'S3': close - range_hl * 1.1 / 4,
'S4': close - range_hl * 1.1 / 2
}
# Demo
pivot_calc = PivotPointCalculator()
yesterday_high = 1.0920
yesterday_low = 1.0845
yesterday_close = 1.0878
print("Standard Pivot Points:")
std_pivots = pivot_calc.standard_pivots(yesterday_high, yesterday_low, yesterday_close)
for level, price in std_pivots.items():
print(f" {level}: {price:.5f}")
print("\nFibonacci Pivot Points:")
fib_pivots = pivot_calc.fibonacci_pivots(yesterday_high, yesterday_low, yesterday_close)
for level, price in fib_pivots.items():
print(f" {level}: {price:.5f}")
Fibonacci Retracements and Extensions
Fibonacci levels are heavily used in forex for identifying potential reversal zones and profit targets.
class FibonacciAnalyzer:
"""Calculate Fibonacci retracements and extensions."""
RETRACEMENT_LEVELS = [0.236, 0.382, 0.5, 0.618, 0.786]
EXTENSION_LEVELS = [1.0, 1.272, 1.618, 2.0, 2.618]
def calculate_retracements(self, swing_high: float, swing_low: float,
trend: str = 'up') -> Dict[str, float]:
"""Calculate Fibonacci retracement levels."""
diff = swing_high - swing_low
levels = {}
for fib in self.RETRACEMENT_LEVELS:
if trend == 'up':
levels[f'{fib:.1%}'] = swing_high - diff * fib
else:
levels[f'{fib:.1%}'] = swing_low + diff * fib
levels['0.0%'] = swing_high if trend == 'up' else swing_low
levels['100.0%'] = swing_low if trend == 'up' else swing_high
return levels
def calculate_extensions(self, swing_high: float, swing_low: float,
retracement_point: float, trend: str = 'up') -> Dict[str, float]:
"""Calculate Fibonacci extension levels for profit targets."""
diff = swing_high - swing_low
levels = {}
for fib in self.EXTENSION_LEVELS:
if trend == 'up':
levels[f'{fib:.1%}'] = retracement_point + diff * fib
else:
levels[f'{fib:.1%}'] = retracement_point - diff * fib
return levels
# Demo
fib_analyzer = FibonacciAnalyzer()
swing_low = 1.0750
swing_high = 1.0950
retracements = fib_analyzer.calculate_retracements(swing_high, swing_low, 'up')
print("Fibonacci Retracements (Uptrend):")
for level, price in sorted(retracements.items(), key=lambda x: x[1], reverse=True):
print(f" {level}: {price:.5f}")
bounce_point = retracements['61.8%']
extensions = fib_analyzer.calculate_extensions(swing_high, swing_low, bounce_point, 'up')
print(f"\nFibonacci Extensions (from bounce at {bounce_point:.5f}):")
for level, price in sorted(extensions.items(), key=lambda x: x[1]):
print(f" {level}: {price:.5f}")
5.3 Multi-Timeframe Analysis
Multi-timeframe analysis (MTF) uses multiple chart timeframes to identify high-probability trades by aligning trend direction across timeframes.
Top-Down Approach
Start with higher timeframes for trend direction, then drill down for entries.
class MultiTimeframeAnalyzer:
"""Analyze price action across multiple timeframes."""
TIMEFRAME_HIERARCHY = ['W', 'D', '4h', '1h', '15min']
def __init__(self):
self.data = {}
def calculate_trend(self, df: pd.DataFrame, ma_period: int = 20) -> str:
"""Determine trend using moving average."""
if len(df) < ma_period:
return 'unknown'
ma = df['close'].rolling(ma_period).mean()
current_price = df['close'].iloc[-1]
current_ma = ma.iloc[-1]
ma_slope = ma.iloc[-1] - ma.iloc[-5] if len(ma) >= 5 else 0
if current_price > current_ma and ma_slope > 0:
return 'bullish'
elif current_price < current_ma and ma_slope < 0:
return 'bearish'
return 'neutral'
def analyze_timeframes(self, base_df: pd.DataFrame) -> Dict[str, Dict]:
"""Analyze trend across multiple timeframes."""
results = {}
if 'timestamp' in base_df.columns:
df = base_df.set_index('timestamp')
else:
df = base_df.copy()
timeframes = {'4h': '4h', 'D': 'D'}
for tf_name, resample_rule in timeframes.items():
try:
tf_data = df.resample(resample_rule).agg({
'open': 'first',
'high': 'max',
'low': 'min',
'close': 'last'
}).dropna().reset_index()
trend = self.calculate_trend(tf_data)
results[tf_name] = {
'trend': trend,
'last_close': tf_data['close'].iloc[-1] if len(tf_data) > 0 else None
}
except:
results[tf_name] = {'trend': 'unknown', 'last_close': None}
return results
def get_alignment_score(self, analysis: Dict[str, Dict]) -> Dict:
"""Calculate how aligned the timeframes are."""
trends = [a['trend'] for a in analysis.values() if a['trend'] != 'unknown']
if not trends:
return {'score': 0, 'direction': 'none', 'aligned': False}
bullish_count = trends.count('bullish')
bearish_count = trends.count('bearish')
total = len(trends)
if bullish_count == total:
return {'score': 100, 'direction': 'bullish', 'aligned': True}
elif bearish_count == total:
return {'score': 100, 'direction': 'bearish', 'aligned': True}
elif bullish_count > bearish_count:
return {'score': (bullish_count / total) * 100, 'direction': 'bullish', 'aligned': False}
return {'score': (bearish_count / total) * 100, 'direction': 'bearish', 'aligned': False}
# Demo
mtf = MultiTimeframeAnalyzer()
analysis = mtf.analyze_timeframes(demo_df)
print("Multi-Timeframe Analysis:")
for tf, data in analysis.items():
print(f" {tf}: {data['trend']}")
alignment = mtf.get_alignment_score(analysis)
print(f"\nAlignment: {alignment['score']:.0f}% {alignment['direction']}")
print(f"Fully Aligned: {alignment['aligned']}")
5.4 Price Action Trading
Price action trading focuses on raw price movements without indicators, using candlestick patterns and key levels.
class CandlestickPatterns:
"""Identify key candlestick patterns."""
def __init__(self, body_threshold: float = 0.3):
self.body_threshold = body_threshold
def get_candle_properties(self, row: pd.Series) -> Dict:
"""Extract candle properties."""
open_p, high, low, close = row['open'], row['high'], row['low'], row['close']
range_size = high - low
body_size = abs(close - open_p)
upper_wick = high - max(open_p, close)
lower_wick = min(open_p, close) - low
return {
'bullish': close > open_p,
'body_size': body_size,
'range': range_size,
'body_pct': body_size / range_size if range_size > 0 else 0,
'upper_wick_pct': upper_wick / range_size if range_size > 0 else 0,
'lower_wick_pct': lower_wick / range_size if range_size > 0 else 0
}
def is_doji(self, props: Dict) -> bool:
"""Check for doji (indecision)."""
return props['body_pct'] < 0.1
def is_hammer(self, props: Dict) -> bool:
"""Check for hammer/hanging man."""
return (props['lower_wick_pct'] > 0.6 and
props['upper_wick_pct'] < 0.1 and
props['body_pct'] < 0.3)
def is_shooting_star(self, props: Dict) -> bool:
"""Check for shooting star/inverted hammer."""
return (props['upper_wick_pct'] > 0.6 and
props['lower_wick_pct'] < 0.1 and
props['body_pct'] < 0.3)
def scan_patterns(self, df: pd.DataFrame, lookback: int = 5) -> List[Dict]:
"""Scan for patterns in recent candles."""
patterns = []
for i in range(-lookback, 0):
row = df.iloc[i]
props = self.get_candle_properties(row)
if self.is_doji(props):
patterns.append({'index': i, 'pattern': 'doji', 'signal': 'reversal_warning'})
elif self.is_hammer(props):
signal = 'bullish' if props['bullish'] else 'potential_bullish'
patterns.append({'index': i, 'pattern': 'hammer', 'signal': signal})
elif self.is_shooting_star(props):
signal = 'bearish' if not props['bullish'] else 'potential_bearish'
patterns.append({'index': i, 'pattern': 'shooting_star', 'signal': signal})
return patterns
# Demo
candle_patterns = CandlestickPatterns()
patterns = candle_patterns.scan_patterns(demo_df)
print("Recent Candlestick Patterns:")
for p in patterns[-5:]:
print(f" Bar {p['index']}: {p['pattern']} ({p['signal']})")
Exercises
Exercise 1: Support/Resistance Level Finder (Guided)
Complete the EnhancedSRFinder class that identifies support and resistance levels and determines if they're currently being tested.
class EnhancedSRFinder:
"""Enhanced support/resistance finder with level testing detection."""
def __init__(self, lookback: int = 20, proximity_pct: float = 0.002):
self.lookback = lookback
self.proximity_pct = proximity_pct
def identify_levels(self, df: pd.DataFrame) -> Dict[str, List[float]]:
"""Identify key S/R levels from swing points."""
resistance_levels = []
support_levels = []
for i in range(self.lookback, len(df) - self.lookback):
window_high = df['high'].iloc[i-self.lookback:i+self.lookback+1]
if df['high'].iloc[i] == ______():
resistance_levels.append(df['high'].iloc[i])
window_low = df['low'].iloc[i-self.lookback:i+self.lookback+1]
if df['low'].iloc[i] == ______():
support_levels.append(df['low'].iloc[i])
return {'resistance': resistance_levels, 'support': support_levels}
def is_testing_level(self, current_price: float, level: float) -> bool:
"""Check if price is testing (near) a level."""
distance_pct = ______(current_price - level) / level
return distance_pct <= self.proximity_pct
def get_nearest_levels(self, df: pd.DataFrame, current_price: float) -> Dict[str, Dict]:
"""Get nearest support and resistance with testing status."""
levels = self.identify_levels(df)
resistance_above = [r for r in levels['resistance'] if r > current_price]
nearest_resistance = ______(resistance_above) if resistance_above else None
support_below = [s for s in levels['support'] if s < current_price]
nearest_support = ______(support_below) if support_below else None
result = {}
if nearest_resistance:
result['resistance'] = {
'level': nearest_resistance,
'is_testing': self.is_testing_level(current_price, nearest_resistance)
}
if nearest_support:
result['support'] = {
'level': nearest_support,
'is_testing': self.is_testing_level(current_price, nearest_support)
}
return result
# Test
sr_finder = EnhancedSRFinder(lookback=10)
current_price = demo_df['close'].iloc[-1]
nearest = sr_finder.get_nearest_levels(demo_df, current_price)
print(f"Current Price: {current_price:.5f}")
if 'resistance' in nearest:
r = nearest['resistance']
print(f"Nearest Resistance: {r['level']:.5f} (Testing: {r['is_testing']})")
if 'support' in nearest:
s = nearest['support']
print(f"Nearest Support: {s['level']:.5f} (Testing: {s['is_testing']})")
Solution 1
class EnhancedSRFinder:
def __init__(self, lookback: int = 20, proximity_pct: float = 0.002):
self.lookback = lookback
self.proximity_pct = proximity_pct
def identify_levels(self, df: pd.DataFrame) -> Dict[str, List[float]]:
resistance_levels = []
support_levels = []
for i in range(self.lookback, len(df) - self.lookback):
window_high = df['high'].iloc[i-self.lookback:i+self.lookback+1]
if df['high'].iloc[i] == window_high.max():
resistance_levels.append(df['high'].iloc[i])
window_low = df['low'].iloc[i-self.lookback:i+self.lookback+1]
if df['low'].iloc[i] == window_low.min():
support_levels.append(df['low'].iloc[i])
return {'resistance': resistance_levels, 'support': support_levels}
def is_testing_level(self, current_price: float, level: float) -> bool:
distance_pct = abs(current_price - level) / level
return distance_pct <= self.proximity_pct
def get_nearest_levels(self, df: pd.DataFrame, current_price: float) -> Dict[str, Dict]:
levels = self.identify_levels(df)
resistance_above = [r for r in levels['resistance'] if r > current_price]
nearest_resistance = min(resistance_above) if resistance_above else None
support_below = [s for s in levels['support'] if s < current_price]
nearest_support = max(support_below) if support_below else None
result = {}
if nearest_resistance:
result['resistance'] = {
'level': nearest_resistance,
'is_testing': self.is_testing_level(current_price, nearest_resistance)
}
if nearest_support:
result['support'] = {
'level': nearest_support,
'is_testing': self.is_testing_level(current_price, nearest_support)
}
return result
Exercise 2: Currency Strength Calculator (Guided)
Complete the QuickStrengthMeter that calculates currency strength using a simplified method.
class QuickStrengthMeter:
"""Simplified currency strength calculator."""
USD_PAIRS = {
'EURUSD': 'quote',
'GBPUSD': 'quote',
'USDJPY': 'base',
'USDCHF': 'base',
'AUDUSD': 'quote',
'USDCAD': 'base'
}
def __init__(self, period: int = 10):
self.period = period
def calculate_usd_strength(self, pair_changes: Dict[str, float]) -> float:
"""Calculate USD strength from pair percentage changes."""
usd_strength = 0.0
pair_count = 0
for pair, position in self.USD_PAIRS.items():
if pair not in pair_changes:
continue
change = pair_changes[pair]
if position == 'base':
usd_strength ______ change
else:
usd_strength ______ change
pair_count += 1
return usd_strength / pair_count if pair_count > 0 else 0.0
def get_pair_changes(self, pair_data: Dict[str, pd.Series]) -> Dict[str, float]:
"""Calculate percentage change for each pair."""
changes = {}
for pair, prices in pair_data.items():
if len(prices) >= self.period:
start_price = prices.iloc[-self.period]
end_price = prices.iloc[-1]
changes[pair] = ((end_price - start_price) / start_price) * ______
return changes
def analyze(self, pair_data: Dict[str, pd.Series]) -> Dict:
"""Full analysis of USD strength."""
changes = self.get_pair_changes(pair_data)
usd_strength = self.calculate_usd_strength(changes)
return {
'usd_strength': usd_strength,
'interpretation': 'bullish' if usd_strength > 0 else 'bearish',
'pair_changes': changes
}
# Test
test_pairs = {}
np.random.seed(123)
for pair in QuickStrengthMeter.USD_PAIRS.keys():
base = 1.10 if 'JPY' not in pair else 150.0
test_pairs[pair] = pd.Series([base * (1 + np.random.normal(0.001, 0.005)) for _ in range(20)])
meter = QuickStrengthMeter()
result = meter.analyze(test_pairs)
print(f"USD Strength: {result['usd_strength']:.3f}%")
print(f"Interpretation: USD is {result['interpretation']}")
Solution 2
class QuickStrengthMeter:
USD_PAIRS = {
'EURUSD': 'quote',
'GBPUSD': 'quote',
'USDJPY': 'base',
'USDCHF': 'base',
'AUDUSD': 'quote',
'USDCAD': 'base'
}
def __init__(self, period: int = 10):
self.period = period
def calculate_usd_strength(self, pair_changes: Dict[str, float]) -> float:
usd_strength = 0.0
pair_count = 0
for pair, position in self.USD_PAIRS.items():
if pair not in pair_changes:
continue
change = pair_changes[pair]
if position == 'base':
usd_strength += change
else:
usd_strength -= change
pair_count += 1
return usd_strength / pair_count if pair_count > 0 else 0.0
def get_pair_changes(self, pair_data: Dict[str, pd.Series]) -> Dict[str, float]:
changes = {}
for pair, prices in pair_data.items():
if len(prices) >= self.period:
start_price = prices.iloc[-self.period]
end_price = prices.iloc[-1]
changes[pair] = ((end_price - start_price) / start_price) * 100
return changes
def analyze(self, pair_data: Dict[str, pd.Series]) -> Dict:
changes = self.get_pair_changes(pair_data)
usd_strength = self.calculate_usd_strength(changes)
return {
'usd_strength': usd_strength,
'interpretation': 'bullish' if usd_strength > 0 else 'bearish',
'pair_changes': changes
}
Exercise 3: Pivot Point Trader (Guided)
Complete the PivotTrader class that generates signals based on pivot point levels.
class PivotTrader:
"""Generate trading signals from pivot points."""
def __init__(self, bounce_threshold: float = 0.0003):
self.bounce_threshold = bounce_threshold
self.pivot_calc = PivotPointCalculator()
def get_position_vs_pivot(self, price: float, pivots: Dict[str, float]) -> str:
"""Determine price position relative to pivot."""
pp = pivots['PP']
if price > pivots.get('R2', pp * 1.01):
return 'above_r2'
elif price > pivots.get('R1', pp * 1.005):
return 'above_r1'
elif price > pp:
return 'above_pp'
elif price > pivots.get('S1', pp * 0.995):
return 'below_pp'
elif price > pivots.get('S2', pp * 0.99):
return 'below_s1'
return 'below_s2'
def check_bounce(self, current: float, prev: float, level: float) -> Optional[str]:
"""Check if price bounced off a level."""
if abs(prev - level) / level < self.bounce_threshold:
if current > prev:
return 'bullish_bounce'
elif current < prev:
return 'bearish_bounce'
return None
def generate_signal(self, current_price: float, prev_price: float,
yesterday_high: float, yesterday_low: float,
yesterday_close: float) -> Dict:
"""Generate trading signal based on pivot levels."""
pivots = self.pivot_calc.______(yesterday_high, yesterday_low, yesterday_close)
position = self.get_position_vs_pivot(current_price, pivots)
signal = {
'position': position,
'pivots': pivots,
'action': 'HOLD',
'reason': ''
}
for level_name in ['PP', 'S1', 'S2', 'R1', 'R2']:
level = pivots.get(level_name)
if level:
bounce = self.check_bounce(current_price, prev_price, level)
if bounce == 'bullish_bounce' and level_name in ['PP', 'S1', 'S2']:
signal['action'] = '______'
signal['reason'] = f'Bullish bounce at {level_name}'
break
elif bounce == 'bearish_bounce' and level_name in ['PP', 'R1', 'R2']:
signal['action'] = '______'
signal['reason'] = f'Bearish bounce at {level_name}'
break
return signal
# Test
trader = PivotTrader()
y_high, y_low, y_close = 1.0920, 1.0845, 1.0878
pivots = PivotPointCalculator().standard_pivots(y_high, y_low, y_close)
prev_price = pivots['S1'] + 0.0001
current_price = pivots['S1'] + 0.0010
signal = trader.generate_signal(current_price, prev_price, y_high, y_low, y_close)
print(f"Current Price: {current_price:.5f}")
print(f"Position: {signal['position']}")
print(f"Action: {signal['action']}")
print(f"Reason: {signal['reason']}")
Solution 3
class PivotTrader:
def __init__(self, bounce_threshold: float = 0.0003):
self.bounce_threshold = bounce_threshold
self.pivot_calc = PivotPointCalculator()
def get_position_vs_pivot(self, price: float, pivots: Dict[str, float]) -> str:
pp = pivots['PP']
if price > pivots.get('R2', pp * 1.01):
return 'above_r2'
elif price > pivots.get('R1', pp * 1.005):
return 'above_r1'
elif price > pp:
return 'above_pp'
elif price > pivots.get('S1', pp * 0.995):
return 'below_pp'
elif price > pivots.get('S2', pp * 0.99):
return 'below_s1'
return 'below_s2'
def check_bounce(self, current: float, prev: float, level: float) -> Optional[str]:
if abs(prev - level) / level < self.bounce_threshold:
if current > prev:
return 'bullish_bounce'
elif current < prev:
return 'bearish_bounce'
return None
def generate_signal(self, current_price: float, prev_price: float,
yesterday_high: float, yesterday_low: float,
yesterday_close: float) -> Dict:
pivots = self.pivot_calc.standard_pivots(yesterday_high, yesterday_low, yesterday_close)
position = self.get_position_vs_pivot(current_price, pivots)
signal = {
'position': position,
'pivots': pivots,
'action': 'HOLD',
'reason': ''
}
for level_name in ['PP', 'S1', 'S2', 'R1', 'R2']:
level = pivots.get(level_name)
if level:
bounce = self.check_bounce(current_price, prev_price, level)
if bounce == 'bullish_bounce' and level_name in ['PP', 'S1', 'S2']:
signal['action'] = 'BUY'
signal['reason'] = f'Bullish bounce at {level_name}'
break
elif bounce == 'bearish_bounce' and level_name in ['PP', 'R1', 'R2']:
signal['action'] = 'SELL'
signal['reason'] = f'Bearish bounce at {level_name}'
break
return signal
Exercise 4: Multi-Timeframe Trend Analyzer (Open-ended)
Build a comprehensive MTF analyzer that: - Analyzes 3+ timeframes (e.g., Daily, 4H, 1H) - Calculates trend direction for each using your choice of method - Returns an alignment score and trading bias - Identifies the best timeframe for entry
# Exercise 4: Multi-Timeframe Trend Analyzer (Open-ended)
#
# Requirements:
# 1. Create class ComprehensiveMTFAnalyzer
# 2. Support at least 3 timeframes
# 3. Calculate trend using MA crossover or ADX
# 4. Return alignment score (0-100)
# 5. Provide trading recommendation
#
# Your implementation:
Solution 4
class ComprehensiveMTFAnalyzer:
"""Multi-timeframe trend analyzer with alignment scoring."""
def __init__(self, timeframes: List[str] = None):
self.timeframes = timeframes or ['D', '4h', '1h']
self.tf_weights = {'D': 3, '4h': 2, '1h': 1}
def calculate_trend(self, df: pd.DataFrame, fast: int = 10, slow: int = 20) -> Dict:
if len(df) < slow:
return {'direction': 'unknown', 'strength': 0}
ma_fast = df['close'].rolling(fast).mean()
ma_slow = df['close'].rolling(slow).mean()
diff = (ma_fast.iloc[-1] - ma_slow.iloc[-1]) / ma_slow.iloc[-1]
if diff > 0.002:
return {'direction': 'bullish', 'strength': min(abs(diff) * 100, 100)}
elif diff < -0.002:
return {'direction': 'bearish', 'strength': min(abs(diff) * 100, 100)}
return {'direction': 'neutral', 'strength': 0}
def analyze(self, df: pd.DataFrame) -> Dict:
results = {}
if 'timestamp' in df.columns:
df = df.set_index('timestamp')
for tf in self.timeframes:
tf_data = df.resample(tf).agg({
'open': 'first', 'high': 'max', 'low': 'min', 'close': 'last'
}).dropna()
results[tf] = self.calculate_trend(tf_data)
bullish_score = sum(
self.tf_weights.get(tf, 1) * r['strength']
for tf, r in results.items() if r['direction'] == 'bullish'
)
bearish_score = sum(
self.tf_weights.get(tf, 1) * r['strength']
for tf, r in results.items() if r['direction'] == 'bearish'
)
total_weight = sum(self.tf_weights.get(tf, 1) for tf in self.timeframes)
if bullish_score > bearish_score:
bias = 'bullish'
alignment = bullish_score / (total_weight * 100) * 100
elif bearish_score > bullish_score:
bias = 'bearish'
alignment = bearish_score / (total_weight * 100) * 100
else:
bias = 'neutral'
alignment = 0
return {
'timeframes': results,
'bias': bias,
'alignment_score': alignment,
'recommendation': 'TRADE' if alignment > 60 else 'WAIT'
}
Exercise 5: Candlestick Pattern Scanner (Open-ended)
Build an advanced candlestick scanner that: - Identifies at least 5 different patterns - Assigns probability scores to each pattern - Considers the context (trend, key levels) - Returns actionable trading signals
# Exercise 5: Candlestick Pattern Scanner (Open-ended)
#
# Requirements:
# 1. Create class AdvancedCandleScanner
# 2. Detect: doji, hammer, engulfing, morning/evening star, three soldiers/crows
# 3. Score pattern quality (0-100)
# 4. Consider if pattern is at key S/R level
# 5. Generate trade signal with entry/stop/target
#
# Your implementation:
Solution 5
class AdvancedCandleScanner:
"""Advanced candlestick pattern scanner with context awareness."""
def __init__(self):
self.sr_finder = SupportResistanceFinder(lookback=10)
def get_candle_type(self, o: float, h: float, l: float, c: float) -> Dict:
body = abs(c - o)
range_size = h - l
upper_wick = h - max(o, c)
lower_wick = min(o, c) - l
return {
'bullish': c > o,
'body_pct': body / range_size if range_size > 0 else 0,
'upper_wick_pct': upper_wick / range_size if range_size > 0 else 0,
'lower_wick_pct': lower_wick / range_size if range_size > 0 else 0,
'body': body,
'range': range_size
}
def detect_patterns(self, df: pd.DataFrame) -> List[Dict]:
patterns = []
for i in range(-5, 0):
row = df.iloc[i]
candle = self.get_candle_type(row['open'], row['high'], row['low'], row['close'])
if candle['body_pct'] < 0.1:
patterns.append({'idx': i, 'pattern': 'doji', 'signal': 'neutral', 'score': 60})
if candle['lower_wick_pct'] > 0.6 and candle['body_pct'] < 0.3:
patterns.append({'idx': i, 'pattern': 'hammer', 'signal': 'bullish', 'score': 70})
if candle['upper_wick_pct'] > 0.6 and candle['body_pct'] < 0.3:
patterns.append({'idx': i, 'pattern': 'shooting_star', 'signal': 'bearish', 'score': 70})
if i > -len(df) + 1:
prev = df.iloc[i-1]
prev_candle = self.get_candle_type(prev['open'], prev['high'], prev['low'], prev['close'])
if candle['body'] > prev_candle['body'] * 1.3:
if candle['bullish'] and not prev_candle['bullish']:
patterns.append({'idx': i, 'pattern': 'bullish_engulfing', 'signal': 'bullish', 'score': 80})
elif not candle['bullish'] and prev_candle['bullish']:
patterns.append({'idx': i, 'pattern': 'bearish_engulfing', 'signal': 'bearish', 'score': 80})
return patterns
def generate_signals(self, df: pd.DataFrame) -> List[Dict]:
patterns = self.detect_patterns(df)
current_price = df['close'].iloc[-1]
atr = (df['high'] - df['low']).rolling(14).mean().iloc[-1]
levels = self.sr_finder.get_key_levels(df)
signals = []
for p in patterns:
if p['idx'] < -2:
continue
at_support = any(abs(current_price - s['level']) / s['level'] < 0.003 for s in levels['support'])
at_resistance = any(abs(current_price - r['level']) / r['level'] < 0.003 for r in levels['resistance'])
score = p['score']
if at_support and p['signal'] == 'bullish':
score += 20
elif at_resistance and p['signal'] == 'bearish':
score += 20
if score >= 70:
signals.append({
'pattern': p['pattern'],
'action': 'BUY' if p['signal'] == 'bullish' else 'SELL',
'score': min(score, 100),
'entry': current_price,
'stop': current_price - atr * 1.5 if p['signal'] == 'bullish' else current_price + atr * 1.5,
'target': current_price + atr * 2 if p['signal'] == 'bullish' else current_price - atr * 2
})
return sorted(signals, key=lambda x: x['score'], reverse=True)
Exercise 6: Complete Technical Analysis Suite (Open-ended)
Build a comprehensive technical analysis class that combines: - Support/resistance levels - Trend analysis - Chart patterns - Candlestick patterns - Multi-timeframe confirmation
Generate a complete market analysis report.
# Exercise 6: Complete Technical Analysis Suite (Open-ended)
#
# Requirements:
# 1. Create class TechnicalAnalysisSuite
# 2. Combine S/R, trend, patterns, candles, MTF
# 3. Generate comprehensive analysis report
# 4. Provide overall bias with confidence
# 5. Output actionable trade setup if conditions align
#
# Your implementation:
Solution 6
class TechnicalAnalysisSuite:
"""Comprehensive technical analysis combining multiple methods."""
def __init__(self):
self.sr_finder = SupportResistanceFinder(lookback=10)
self.pattern_recognizer = ChartPatternRecognizer()
self.candle_scanner = CandlestickPatterns()
self.mtf_analyzer = MultiTimeframeAnalyzer()
def analyze_trend(self, df: pd.DataFrame) -> Dict:
ma20 = df['close'].rolling(20).mean()
ma50 = df['close'].rolling(50).mean()
current = df['close'].iloc[-1]
if len(ma50.dropna()) < 1:
return {'direction': 'unknown', 'strength': 0}
if current > ma20.iloc[-1] > ma50.iloc[-1]:
return {'direction': 'bullish', 'strength': 80}
elif current < ma20.iloc[-1] < ma50.iloc[-1]:
return {'direction': 'bearish', 'strength': 80}
return {'direction': 'neutral', 'strength': 30}
def full_analysis(self, df: pd.DataFrame) -> Dict:
report = {
'timestamp': pd.Timestamp.now(),
'current_price': df['close'].iloc[-1],
'trend': self.analyze_trend(df),
'key_levels': self.sr_finder.get_key_levels(df),
'chart_patterns': self.pattern_recognizer.scan_patterns(df),
'candle_patterns': self.candle_scanner.scan_patterns(df, lookback=3),
'mtf': self.mtf_analyzer.analyze_timeframes(df)
}
bullish_signals = bearish_signals = 0
if report['trend']['direction'] == 'bullish':
bullish_signals += 2
elif report['trend']['direction'] == 'bearish':
bearish_signals += 2
for p in report['chart_patterns']:
if p['signal'] == 'bullish': bullish_signals += 1
elif p['signal'] == 'bearish': bearish_signals += 1
for c in report['candle_patterns']:
if 'bullish' in c.get('signal', ''): bullish_signals += 1
elif 'bearish' in c.get('signal', ''): bearish_signals += 1
total = bullish_signals + bearish_signals
if total > 0:
if bullish_signals > bearish_signals:
report['overall_bias'] = 'bullish'
report['confidence'] = bullish_signals / total * 100
else:
report['overall_bias'] = 'bearish'
report['confidence'] = bearish_signals / total * 100
else:
report['overall_bias'] = 'neutral'
report['confidence'] = 0
if report['confidence'] > 60:
atr = (df['high'] - df['low']).rolling(14).mean().iloc[-1]
entry = report['current_price']
if report['overall_bias'] == 'bullish':
report['trade_setup'] = {
'action': 'BUY', 'entry': entry,
'stop': entry - atr * 1.5, 'target': entry + atr * 2.5
}
else:
report['trade_setup'] = {
'action': 'SELL', 'entry': entry,
'stop': entry + atr * 1.5, 'target': entry - atr * 2.5
}
else:
report['trade_setup'] = None
return report
Module Project: Technical Analysis Suite
Build a production-ready technical analysis system that combines all concepts from this module.
class ForexTechnicalAnalyzer:
"""
Production-ready forex technical analysis system.
Combines: S/R detection, Trend analysis, Chart patterns,
Candlestick patterns, MTF confirmation, Pivot points, Fibonacci.
"""
def __init__(self, pair: str = 'EURUSD'):
self.pair = pair
self.sr_finder = SupportResistanceFinder(lookback=15)
self.pattern_detector = ChartPatternRecognizer()
self.candle_scanner = CandlestickPatterns()
self.mtf_analyzer = MultiTimeframeAnalyzer()
self.pivot_calc = PivotPointCalculator()
self.fib_analyzer = FibonacciAnalyzer()
def analyze_structure(self, df: pd.DataFrame) -> Dict:
"""Analyze market structure."""
levels = self.sr_finder.get_key_levels(df)
current = df['close'].iloc[-1]
resistance_above = [r for r in levels['resistance'] if r['level'] > current]
support_below = [s for s in levels['support'] if s['level'] < current]
return {
'current_price': current,
'nearest_resistance': min(resistance_above, key=lambda x: x['level']) if resistance_above else None,
'nearest_support': max(support_below, key=lambda x: x['level']) if support_below else None
}
def analyze_trend(self, df: pd.DataFrame) -> Dict:
"""Comprehensive trend analysis."""
ma10 = df['close'].rolling(10).mean()
ma20 = df['close'].rolling(20).mean()
ma50 = df['close'].rolling(50).mean()
current = df['close'].iloc[-1]
if current > ma10.iloc[-1] > ma20.iloc[-1] > ma50.iloc[-1]:
return {'direction': 'strong_bullish', 'strength': 100}
elif current > ma20.iloc[-1] > ma50.iloc[-1]:
return {'direction': 'bullish', 'strength': 75}
elif current < ma10.iloc[-1] < ma20.iloc[-1] < ma50.iloc[-1]:
return {'direction': 'strong_bearish', 'strength': 100}
elif current < ma20.iloc[-1] < ma50.iloc[-1]:
return {'direction': 'bearish', 'strength': 75}
return {'direction': 'ranging', 'strength': 30}
def calculate_bias(self, structure: Dict, trend: Dict, patterns: Dict, pivots: Dict) -> Dict:
"""Calculate overall trading bias."""
score = 0
factors = []
if 'bullish' in trend['direction']:
score += 3 * (trend['strength'] / 100)
factors.append(f"Trend: {trend['direction']}")
elif 'bearish' in trend['direction']:
score -= 3 * (trend['strength'] / 100)
factors.append(f"Trend: {trend['direction']}")
for p in patterns.get('chart_patterns', []):
if p['signal'] == 'bullish':
score += 2
factors.append(f"Chart: {p['pattern']}")
elif p['signal'] == 'bearish':
score -= 2
factors.append(f"Chart: {p['pattern']}")
if pivots:
current = structure['current_price']
pp = pivots.get('PP', current)
if current > pp:
score += 1
factors.append("Above pivot")
else:
score -= 1
factors.append("Below pivot")
if score > 2:
bias = 'bullish'
confidence = min(score * 15, 100)
elif score < -2:
bias = 'bearish'
confidence = min(abs(score) * 15, 100)
else:
bias = 'neutral'
confidence = 30
return {'bias': bias, 'score': score, 'confidence': confidence, 'factors': factors}
def generate_trade_setup(self, analysis: Dict) -> Optional[Dict]:
"""Generate specific trade setup if conditions align."""
bias = analysis['bias']
if bias['confidence'] < 50:
return None
current = analysis['structure']['current_price']
atr = analysis.get('atr', current * 0.01)
if bias['bias'] == 'bullish':
support = analysis['structure'].get('nearest_support')
stop = support['level'] * 0.998 if support else current - atr * 1.5
resistance = analysis['structure'].get('nearest_resistance')
risk = current - stop
target = resistance['level'] if resistance else current + risk * 2
return {
'action': 'BUY', 'entry': current, 'stop_loss': stop, 'take_profit': target,
'risk_pips': (current - stop) * 10000, 'reward_pips': (target - current) * 10000,
'risk_reward': (target - current) / (current - stop) if current != stop else 0
}
elif bias['bias'] == 'bearish':
resistance = analysis['structure'].get('nearest_resistance')
stop = resistance['level'] * 1.002 if resistance else current + atr * 1.5
support = analysis['structure'].get('nearest_support')
risk = stop - current
target = support['level'] if support else current - risk * 2
return {
'action': 'SELL', 'entry': current, 'stop_loss': stop, 'take_profit': target,
'risk_pips': (stop - current) * 10000, 'reward_pips': (current - target) * 10000,
'risk_reward': (current - target) / (stop - current) if stop != current else 0
}
return None
def full_analysis(self, df: pd.DataFrame) -> Dict:
"""Run complete technical analysis."""
atr = (df['high'] - df['low']).rolling(14).mean().iloc[-1]
structure = self.analyze_structure(df)
trend = self.analyze_trend(df)
patterns = {
'chart_patterns': self.pattern_detector.scan_patterns(df),
'candle_patterns': self.candle_scanner.scan_patterns(df, lookback=5)
}
if 'timestamp' in df.columns:
df_indexed = df.set_index('timestamp')
else:
df_indexed = df
daily = df_indexed.resample('D').agg({
'open': 'first', 'high': 'max', 'low': 'min', 'close': 'last'
}).dropna()
if len(daily) >= 2:
yesterday = daily.iloc[-2]
pivots = self.pivot_calc.standard_pivots(yesterday['high'], yesterday['low'], yesterday['close'])
else:
pivots = {}
bias = self.calculate_bias(structure, trend, patterns, pivots)
analysis = {
'pair': self.pair, 'timestamp': pd.Timestamp.now(),
'structure': structure, 'trend': trend, 'patterns': patterns,
'pivots': pivots, 'bias': bias, 'atr': atr
}
analysis['trade_setup'] = self.generate_trade_setup(analysis)
return analysis
def print_report(self, analysis: Dict) -> None:
"""Print formatted analysis report."""
print("\n" + "=" * 60)
print(f" TECHNICAL ANALYSIS: {analysis['pair']}")
print(f" {analysis['timestamp'].strftime('%Y-%m-%d %H:%M')}")
print("=" * 60)
print(f"\nCurrent Price: {analysis['structure']['current_price']:.5f}")
if analysis['structure']['nearest_resistance']:
r = analysis['structure']['nearest_resistance']
print(f"Nearest Resistance: {r['level']:.5f} ({r['strength']})")
if analysis['structure']['nearest_support']:
s = analysis['structure']['nearest_support']
print(f"Nearest Support: {s['level']:.5f} ({s['strength']})")
print(f"\nTrend: {analysis['trend']['direction'].upper()} ({analysis['trend']['strength']}%)")
if analysis['pivots']:
print(f"\nPivot Points:")
for level in ['R2', 'R1', 'PP', 'S1', 'S2']:
if level in analysis['pivots']:
print(f" {level}: {analysis['pivots'][level]:.5f}")
print(f"\n" + "-" * 40)
print(f"OVERALL BIAS: {analysis['bias']['bias'].upper()}")
print(f"Confidence: {analysis['bias']['confidence']:.0f}%")
print(f"Factors: {', '.join(analysis['bias']['factors'][:3])}")
if analysis['trade_setup']:
setup = analysis['trade_setup']
print(f"\n" + "=" * 40)
print(f"TRADE SETUP: {setup['action']}")
print(f"Entry: {setup['entry']:.5f}")
print(f"Stop: {setup['stop_loss']:.5f} ({setup['risk_pips']:.1f} pips)")
print(f"Target: {setup['take_profit']:.5f} ({setup['reward_pips']:.1f} pips)")
print(f"Risk/Reward: {setup['risk_reward']:.2f}")
else:
print(f"\nNo trade setup - conditions not aligned")
print("\n" + "=" * 60)
# Run the complete analysis
analyzer = ForexTechnicalAnalyzer(pair='EURUSD')
analysis = analyzer.full_analysis(demo_df)
analyzer.print_report(analysis)
Key Takeaways
- Support/Resistance: Identify key levels where price historically reverses
- Trend Lines: Connect swing points to visualize trend direction and breakouts
- Chart Patterns: Double tops/bottoms, triangles signal potential reversals or continuations
- Currency Strength: Compare relative strength across pairs for high-probability trades
- Pivot Points: Daily levels that act as support/resistance for intraday trading
- Fibonacci: Identify retracement zones and extension targets
- Multi-Timeframe: Align higher timeframe trend with lower timeframe entries
- Price Action: Trade candlestick patterns at key levels without indicators
- Confluence: Combine multiple methods for higher confidence signals
Next: Module 6 - Fundamental Analysis where we'll analyze economic indicators, central bank policies, and build economic calendars for forex trading.
Module 6: Fundamental Analysis for Forex & Futures
Part 2: Analysis & Strategies
| Duration | Exercises |
|---|---|
| ~2.5 hours | 6 |
Learning Objectives
- Understand key economic indicators and their impact on currencies
- Track central bank policies and interest rate decisions
- Build and use economic calendars for trading
- Analyze intermarket relationships between currencies, bonds, and commodities
Prerequisites
- Modules 1-5 (Forex/Futures fundamentals, technical analysis)
- Basic understanding of macroeconomics
- Python pandas proficiency
import pandas as pd
import numpy as np
from datetime import datetime, timedelta
from dataclasses import dataclass, field
from typing import List, Dict, Tuple, Optional
from enum import Enum
import warnings
warnings.filterwarnings('ignore')
6.1 Economic Indicators
Economic indicators are statistics that provide insights into a country's economic health. They directly impact currency valuations and are essential for fundamental forex analysis.
Types of Economic Indicators
| Type | Description | Examples |
|---|---|---|
| Leading | Predict future economic activity | PMI, Building Permits, Yield Curve |
| Coincident | Reflect current economic state | GDP, Employment, Retail Sales |
| Lagging | Confirm trends after they occur | Unemployment Rate, CPI, Interest Rates |
class EconomicIndicator:
"""Represents an economic indicator with its properties and impact."""
IMPACT_LEVELS = {'high': 3, 'medium': 2, 'low': 1}
def __init__(self, name: str, country: str, impact: str = 'medium'):
self.name = name
self.country = country
self.impact = impact
self.history: List[Dict] = []
def add_release(self, date: datetime, actual: float,
forecast: float, previous: float) -> None:
"""Add a new data release."""
surprise = actual - forecast
surprise_pct = (surprise / abs(forecast) * 100) if forecast != 0 else 0
self.history.append({
'date': date,
'actual': actual,
'forecast': forecast,
'previous': previous,
'surprise': surprise,
'surprise_pct': surprise_pct,
'beat': actual > forecast
})
def get_trend(self, periods: int = 4) -> str:
"""Determine recent trend in indicator."""
if len(self.history) < periods:
return 'insufficient_data'
recent = self.history[-periods:]
values = [r['actual'] for r in recent]
increases = sum(1 for i in range(1, len(values)) if values[i] > values[i-1])
if increases >= periods - 1:
return 'improving'
elif increases <= 1:
return 'deteriorating'
return 'mixed'
def surprise_history(self) -> Dict:
"""Analyze history of forecast surprises."""
if not self.history:
return {}
surprises = [r['surprise_pct'] for r in self.history]
beats = sum(1 for r in self.history if r['beat'])
return {
'avg_surprise': np.mean(surprises),
'beat_rate': beats / len(self.history) * 100,
'largest_beat': max(surprises),
'largest_miss': min(surprises)
}
# Demo: US Non-Farm Payrolls
nfp = EconomicIndicator('Non-Farm Payrolls', 'US', 'high')
# Add historical releases
nfp.add_release(datetime(2024, 1, 5), 216, 175, 173)
nfp.add_release(datetime(2024, 2, 2), 353, 185, 216)
nfp.add_release(datetime(2024, 3, 8), 275, 200, 353)
nfp.add_release(datetime(2024, 4, 5), 303, 212, 275)
print(f"Indicator: {nfp.name} ({nfp.country})")
print(f"Impact: {nfp.impact}")
print(f"Trend: {nfp.get_trend()}")
print(f"\nSurprise Analysis:")
for k, v in nfp.surprise_history().items():
print(f" {k}: {v:.1f}")
Key Economic Indicators for Forex
GDP (Gross Domestic Product)
- Measures total economic output
- Released quarterly
- Higher GDP growth = stronger currency (generally)
Inflation (CPI/PCE)
- Consumer Price Index measures price changes
- Higher inflation may lead to rate hikes = currency strength
- Too high inflation can weaken confidence
Employment
- Non-Farm Payrolls (US) - most watched indicator
- Unemployment Rate
- Strong employment = strong economy = strong currency
class EconomicIndicatorTracker:
"""Track multiple economic indicators for a country."""
# Standard indicators by importance
STANDARD_INDICATORS = {
'US': [
('Non-Farm Payrolls', 'high'),
('CPI YoY', 'high'),
('GDP QoQ', 'high'),
('Fed Interest Rate', 'high'),
('Retail Sales MoM', 'medium'),
('ISM Manufacturing PMI', 'medium'),
('Unemployment Rate', 'medium'),
],
'EU': [
('ECB Interest Rate', 'high'),
('CPI YoY', 'high'),
('GDP QoQ', 'high'),
('German ZEW Sentiment', 'medium'),
('Unemployment Rate', 'medium'),
],
'UK': [
('BoE Interest Rate', 'high'),
('CPI YoY', 'high'),
('GDP QoQ', 'high'),
('Retail Sales', 'medium'),
],
'JP': [
('BoJ Interest Rate', 'high'),
('CPI YoY', 'medium'),
('GDP QoQ', 'medium'),
('Tankan Survey', 'medium'),
]
}
def __init__(self, country: str):
self.country = country
self.indicators: Dict[str, EconomicIndicator] = {}
self._initialize_indicators()
def _initialize_indicators(self) -> None:
"""Initialize standard indicators for country."""
if self.country in self.STANDARD_INDICATORS:
for name, impact in self.STANDARD_INDICATORS[self.country]:
self.indicators[name] = EconomicIndicator(name, self.country, impact)
def update_indicator(self, name: str, date: datetime,
actual: float, forecast: float, previous: float) -> None:
"""Update an indicator with new data."""
if name in self.indicators:
self.indicators[name].add_release(date, actual, forecast, previous)
def get_economic_score(self) -> Dict:
"""Calculate overall economic health score."""
total_score = 0
max_score = 0
details = []
for name, indicator in self.indicators.items():
weight = EconomicIndicator.IMPACT_LEVELS[indicator.impact]
max_score += weight * 100
if indicator.history:
latest = indicator.history[-1]
# Score based on beat/miss and trend
score = 50 # neutral base
if latest['beat']:
score += min(latest['surprise_pct'] * 2, 30)
else:
score -= min(abs(latest['surprise_pct']) * 2, 30)
trend = indicator.get_trend()
if trend == 'improving':
score += 20
elif trend == 'deteriorating':
score -= 20
score = max(0, min(100, score))
total_score += score * weight
details.append({
'indicator': name,
'score': score,
'trend': trend,
'last_beat': latest['beat']
})
return {
'country': self.country,
'overall_score': (total_score / max_score * 100) if max_score > 0 else 0,
'interpretation': self._interpret_score(total_score / max_score * 100 if max_score > 0 else 0),
'details': details
}
def _interpret_score(self, score: float) -> str:
"""Interpret the economic score."""
if score >= 70:
return 'strong_bullish'
elif score >= 55:
return 'bullish'
elif score >= 45:
return 'neutral'
elif score >= 30:
return 'bearish'
return 'strong_bearish'
# Demo
us_tracker = EconomicIndicatorTracker('US')
# Add some data
us_tracker.update_indicator('Non-Farm Payrolls', datetime(2024, 4, 5), 303, 212, 275)
us_tracker.update_indicator('CPI YoY', datetime(2024, 4, 10), 3.5, 3.4, 3.2)
us_tracker.update_indicator('GDP QoQ', datetime(2024, 4, 25), 1.6, 2.5, 3.4)
score = us_tracker.get_economic_score()
print(f"US Economic Score: {score['overall_score']:.1f}")
print(f"Interpretation: {score['interpretation']}")
6.2 Central Banks
Central banks are the most influential institutions for currency markets. Their monetary policy decisions directly impact currency valuations.
Major Central Banks
| Central Bank | Currency | Key Policy Tool |
|---|---|---|
| Federal Reserve (Fed) | USD | Federal Funds Rate |
| European Central Bank (ECB) | EUR | Main Refinancing Rate |
| Bank of England (BoE) | GBP | Bank Rate |
| Bank of Japan (BoJ) | JPY | Policy Rate, YCC |
| Swiss National Bank (SNB) | CHF | Policy Rate |
| Reserve Bank of Australia (RBA) | AUD | Cash Rate |
| Bank of Canada (BoC) | CAD | Overnight Rate |
| Reserve Bank of New Zealand (RBNZ) | NZD | Official Cash Rate |
class CentralBank:
"""Model a central bank and its monetary policy."""
def __init__(self, name: str, currency: str, current_rate: float):
self.name = name
self.currency = currency
self.current_rate = current_rate
self.rate_history: List[Dict] = []
self.statements: List[Dict] = []
def add_rate_decision(self, date: datetime, rate: float,
expected: float, statement_tone: str = 'neutral') -> None:
"""Record a rate decision."""
change = rate - self.current_rate
surprise = rate - expected
self.rate_history.append({
'date': date,
'rate': rate,
'previous': self.current_rate,
'change': change,
'expected': expected,
'surprise': surprise,
'action': 'hike' if change > 0 else ('cut' if change < 0 else 'hold'),
'tone': statement_tone
})
self.current_rate = rate
def get_policy_stance(self) -> str:
"""Determine current policy stance."""
if len(self.rate_history) < 2:
return 'unknown'
recent = self.rate_history[-3:] if len(self.rate_history) >= 3 else self.rate_history
hikes = sum(1 for r in recent if r['action'] == 'hike')
cuts = sum(1 for r in recent if r['action'] == 'cut')
if hikes > cuts:
return 'hawkish'
elif cuts > hikes:
return 'dovish'
return 'neutral'
def rate_differential(self, other: 'CentralBank') -> float:
"""Calculate rate differential with another central bank."""
return self.current_rate - other.current_rate
def expected_path(self, meetings: int = 4) -> List[Dict]:
"""Project expected rate path based on recent trend."""
stance = self.get_policy_stance()
path = []
rate = self.current_rate
for i in range(meetings):
if stance == 'hawkish':
rate += 0.25 if i < 2 else 0
elif stance == 'dovish':
rate -= 0.25 if i < 2 else 0
path.append({
'meeting': i + 1,
'projected_rate': rate
})
return path
# Demo
fed = CentralBank('Federal Reserve', 'USD', 5.25)
ecb = CentralBank('European Central Bank', 'EUR', 4.50)
# Add rate history
fed.add_rate_decision(datetime(2024, 1, 31), 5.25, 5.25, 'neutral')
fed.add_rate_decision(datetime(2024, 3, 20), 5.25, 5.25, 'hawkish')
ecb.add_rate_decision(datetime(2024, 1, 25), 4.50, 4.50, 'neutral')
ecb.add_rate_decision(datetime(2024, 3, 7), 4.50, 4.50, 'dovish')
print(f"Fed current rate: {fed.current_rate}%")
print(f"Fed stance: {fed.get_policy_stance()}")
print(f"\nECB current rate: {ecb.current_rate}%")
print(f"ECB stance: {ecb.get_policy_stance()}")
print(f"\nUSD-EUR rate differential: {fed.rate_differential(ecb):.2f}%")
class CentralBankMonitor:
"""Monitor multiple central banks for trading signals."""
CURRENCY_PAIRS = {
('USD', 'EUR'): 'EURUSD',
('USD', 'GBP'): 'GBPUSD',
('USD', 'JPY'): 'USDJPY',
('USD', 'CHF'): 'USDCHF',
('USD', 'AUD'): 'AUDUSD',
('USD', 'CAD'): 'USDCAD',
('EUR', 'GBP'): 'EURGBP',
('EUR', 'JPY'): 'EURJPY',
}
def __init__(self):
self.banks: Dict[str, CentralBank] = {}
def add_bank(self, bank: CentralBank) -> None:
"""Add a central bank to monitor."""
self.banks[bank.currency] = bank
def get_all_differentials(self) -> pd.DataFrame:
"""Calculate all rate differentials."""
data = []
currencies = list(self.banks.keys())
for i, curr1 in enumerate(currencies):
for curr2 in currencies[i+1:]:
bank1 = self.banks[curr1]
bank2 = self.banks[curr2]
diff = bank1.rate_differential(bank2)
pair_key = (curr1, curr2) if (curr1, curr2) in self.CURRENCY_PAIRS else (curr2, curr1)
pair = self.CURRENCY_PAIRS.get(pair_key, f"{curr1}{curr2}")
data.append({
'pair': pair,
'currency_1': curr1,
'currency_2': curr2,
'rate_1': bank1.current_rate,
'rate_2': bank2.current_rate,
'differential': diff,
'carry_direction': curr1 if diff > 0 else curr2
})
return pd.DataFrame(data)
def policy_divergence_signals(self) -> List[Dict]:
"""Find trading opportunities from policy divergence."""
signals = []
currencies = list(self.banks.keys())
for i, curr1 in enumerate(currencies):
for curr2 in currencies[i+1:]:
bank1 = self.banks[curr1]
bank2 = self.banks[curr2]
stance1 = bank1.get_policy_stance()
stance2 = bank2.get_policy_stance()
# Look for divergence
if stance1 == 'hawkish' and stance2 == 'dovish':
signals.append({
'long': curr1,
'short': curr2,
'reason': f'{bank1.name} hawkish vs {bank2.name} dovish',
'strength': 'strong'
})
elif stance1 == 'dovish' and stance2 == 'hawkish':
signals.append({
'long': curr2,
'short': curr1,
'reason': f'{bank2.name} hawkish vs {bank1.name} dovish',
'strength': 'strong'
})
return signals
# Demo
monitor = CentralBankMonitor()
monitor.add_bank(fed)
monitor.add_bank(ecb)
monitor.add_bank(CentralBank('Bank of Japan', 'JPY', 0.10))
print("Rate Differentials:")
print(monitor.get_all_differentials().to_string(index=False))
print("\nPolicy Divergence Signals:")
for signal in monitor.policy_divergence_signals():
print(f" Long {signal['long']}, Short {signal['short']}")
print(f" Reason: {signal['reason']}")
6.3 Economic Calendar
An economic calendar tracks scheduled data releases and events. It's essential for avoiding unexpected volatility and finding trading opportunities.
from enum import Enum
class EventImpact(Enum):
LOW = 1
MEDIUM = 2
HIGH = 3
@dataclass
class CalendarEvent:
"""Represents an economic calendar event."""
name: str
country: str
datetime: datetime
impact: EventImpact
forecast: Optional[float] = None
previous: Optional[float] = None
actual: Optional[float] = None
@property
def is_released(self) -> bool:
return self.actual is not None
@property
def surprise(self) -> Optional[float]:
if self.actual is not None and self.forecast is not None:
return self.actual - self.forecast
return None
@property
def beat_forecast(self) -> Optional[bool]:
if self.surprise is not None:
return self.surprise > 0
return None
# Demo
nfp_event = CalendarEvent(
name='Non-Farm Payrolls',
country='US',
datetime=datetime(2024, 5, 3, 8, 30),
impact=EventImpact.HIGH,
forecast=240.0,
previous=303.0
)
print(f"Event: {nfp_event.name}")
print(f"Time: {nfp_event.datetime}")
print(f"Impact: {nfp_event.impact.name}")
print(f"Forecast: {nfp_event.forecast}K")
print(f"Released: {nfp_event.is_released}")
class EconomicCalendar:
"""Manage economic calendar events."""
# Standard high-impact events
HIGH_IMPACT_EVENTS = {
'US': ['Non-Farm Payrolls', 'CPI', 'FOMC Rate Decision', 'GDP', 'Retail Sales'],
'EU': ['ECB Rate Decision', 'CPI', 'GDP', 'German ZEW'],
'UK': ['BoE Rate Decision', 'CPI', 'GDP', 'Employment'],
'JP': ['BoJ Rate Decision', 'CPI', 'GDP', 'Tankan'],
'AU': ['RBA Rate Decision', 'Employment', 'CPI'],
'CA': ['BoC Rate Decision', 'Employment', 'CPI'],
}
def __init__(self):
self.events: List[CalendarEvent] = []
def add_event(self, event: CalendarEvent) -> None:
"""Add event to calendar."""
self.events.append(event)
self.events.sort(key=lambda x: x.datetime)
def get_upcoming(self, hours: int = 24,
min_impact: EventImpact = EventImpact.LOW) -> List[CalendarEvent]:
"""Get upcoming events within time window."""
now = datetime.now()
cutoff = now + timedelta(hours=hours)
return [
e for e in self.events
if now <= e.datetime <= cutoff
and e.impact.value >= min_impact.value
and not e.is_released
]
def get_by_country(self, country: str) -> List[CalendarEvent]:
"""Get events for specific country."""
return [e for e in self.events if e.country == country]
def get_high_impact_today(self) -> List[CalendarEvent]:
"""Get today's high-impact events."""
today = datetime.now().date()
return [
e for e in self.events
if e.datetime.date() == today
and e.impact == EventImpact.HIGH
]
def currency_risk_score(self, currency: str, hours: int = 24) -> Dict:
"""Calculate event risk score for a currency."""
# Map currencies to countries
currency_country = {
'USD': 'US', 'EUR': 'EU', 'GBP': 'UK', 'JPY': 'JP',
'AUD': 'AU', 'CAD': 'CA', 'CHF': 'CH', 'NZD': 'NZ'
}
country = currency_country.get(currency, currency)
upcoming = self.get_upcoming(hours)
country_events = [e for e in upcoming if e.country == country]
risk_score = sum(e.impact.value for e in country_events)
return {
'currency': currency,
'event_count': len(country_events),
'risk_score': risk_score,
'high_impact_events': [e.name for e in country_events if e.impact == EventImpact.HIGH],
'recommendation': 'avoid' if risk_score >= 5 else ('caution' if risk_score >= 3 else 'normal')
}
def to_dataframe(self) -> pd.DataFrame:
"""Convert calendar to DataFrame."""
return pd.DataFrame([
{
'datetime': e.datetime,
'country': e.country,
'event': e.name,
'impact': e.impact.name,
'forecast': e.forecast,
'previous': e.previous,
'actual': e.actual
}
for e in self.events
])
# Demo: Create sample calendar
calendar = EconomicCalendar()
# Add some events
base_date = datetime.now().replace(hour=8, minute=30, second=0, microsecond=0)
calendar.add_event(CalendarEvent(
'Non-Farm Payrolls', 'US', base_date + timedelta(hours=2),
EventImpact.HIGH, forecast=240, previous=303
))
calendar.add_event(CalendarEvent(
'ECB Rate Decision', 'EU', base_date + timedelta(hours=5),
EventImpact.HIGH, forecast=4.50, previous=4.50
))
calendar.add_event(CalendarEvent(
'US Retail Sales', 'US', base_date + timedelta(hours=3),
EventImpact.MEDIUM, forecast=0.4, previous=0.6
))
calendar.add_event(CalendarEvent(
'German ZEW', 'EU', base_date + timedelta(hours=1),
EventImpact.MEDIUM, forecast=42.5, previous=38.4
))
print("Upcoming High Impact Events (24h):")
for event in calendar.get_upcoming(24, EventImpact.HIGH):
print(f" {event.datetime.strftime('%H:%M')} - {event.country}: {event.name}")
print("\nUSD Risk Assessment:")
risk = calendar.currency_risk_score('USD')
print(f" Events: {risk['event_count']}")
print(f" Risk Score: {risk['risk_score']}")
print(f" Recommendation: {risk['recommendation']}")
6.4 Intermarket Analysis
Intermarket analysis examines relationships between different asset classes to gain insights into currency movements.
Key Intermarket Relationships
| Relationship | Correlation | Explanation |
|---|---|---|
| USD vs Gold | Negative | Gold priced in USD; weak USD = expensive gold |
| AUD vs Gold | Positive | Australia major gold exporter |
| CAD vs Oil | Positive | Canada major oil exporter |
| USD vs US Yields | Positive | Higher yields attract USD investment |
| JPY vs Risk | Negative | JPY is safe haven; strengthens in risk-off |
class IntermarketAnalyzer:
"""Analyze relationships between currencies and other markets."""
# Known correlations (positive = move together, negative = inverse)
KNOWN_CORRELATIONS = {
('AUD', 'GOLD'): 0.7,
('CAD', 'OIL'): 0.6,
('USD', 'GOLD'): -0.5,
('USD', 'US10Y'): 0.6,
('JPY', 'VIX'): 0.5, # Safe haven
('CHF', 'VIX'): 0.4, # Safe haven
('AUD', 'SPX'): 0.5, # Risk currency
('NZD', 'SPX'): 0.4, # Risk currency
}
def __init__(self):
self.price_data: Dict[str, pd.Series] = {}
def add_data(self, symbol: str, prices: pd.Series) -> None:
"""Add price data for an asset."""
self.price_data[symbol] = prices
def calculate_returns(self, symbol: str, period: int = 1) -> pd.Series:
"""Calculate returns for an asset."""
if symbol not in self.price_data:
return pd.Series()
return self.price_data[symbol].pct_change(period)
def calculate_correlation(self, symbol1: str, symbol2: str,
window: int = 20) -> float:
"""Calculate rolling correlation between two assets."""
if symbol1 not in self.price_data or symbol2 not in self.price_data:
return 0.0
returns1 = self.calculate_returns(symbol1)
returns2 = self.calculate_returns(symbol2)
# Align data
aligned = pd.concat([returns1, returns2], axis=1).dropna()
if len(aligned) < window:
return 0.0
return aligned.iloc[:, 0].corr(aligned.iloc[:, 1])
def get_correlation_matrix(self) -> pd.DataFrame:
"""Get correlation matrix for all assets."""
symbols = list(self.price_data.keys())
returns_df = pd.DataFrame({
sym: self.calculate_returns(sym) for sym in symbols
}).dropna()
return returns_df.corr()
def check_divergence(self, currency: str, related_asset: str,
lookback: int = 10) -> Optional[Dict]:
"""Check for divergence between currency and related asset."""
expected_corr = self.KNOWN_CORRELATIONS.get((currency, related_asset), 0)
if expected_corr == 0:
return None
actual_corr = self.calculate_correlation(currency, related_asset, lookback)
# Check if correlation has flipped or weakened significantly
if expected_corr > 0 and actual_corr < 0:
divergence = 'negative_flip'
elif expected_corr < 0 and actual_corr > 0:
divergence = 'positive_flip'
elif abs(actual_corr) < abs(expected_corr) * 0.5:
divergence = 'weakened'
else:
divergence = 'normal'
return {
'currency': currency,
'related_asset': related_asset,
'expected_correlation': expected_corr,
'actual_correlation': actual_corr,
'divergence': divergence,
'signal': divergence != 'normal'
}
def generate_signals(self) -> List[Dict]:
"""Generate trading signals from intermarket analysis."""
signals = []
for (currency, asset), expected_corr in self.KNOWN_CORRELATIONS.items():
if currency in self.price_data and asset in self.price_data:
divergence = self.check_divergence(currency, asset)
if divergence and divergence['signal']:
signals.append(divergence)
return signals
# Demo with simulated data
np.random.seed(42)
dates = pd.date_range('2024-01-01', periods=60, freq='D')
analyzer = IntermarketAnalyzer()
# Simulate correlated data
gold = pd.Series([2000 + np.cumsum(np.random.normal(0, 10, 60))[i] for i in range(60)], index=dates)
aud = pd.Series([0.65 + np.cumsum(np.random.normal(0, 0.002, 60))[i] + gold.pct_change().fillna(0).cumsum()[i] * 0.3 for i in range(60)], index=dates)
oil = pd.Series([75 + np.cumsum(np.random.normal(0, 1, 60))[i] for i in range(60)], index=dates)
cad = pd.Series([1.35 + np.cumsum(np.random.normal(0, 0.002, 60))[i] - oil.pct_change().fillna(0).cumsum()[i] * 0.2 for i in range(60)], index=dates)
analyzer.add_data('GOLD', gold)
analyzer.add_data('AUD', aud)
analyzer.add_data('OIL', oil)
analyzer.add_data('CAD', cad)
print("Correlation Matrix:")
print(analyzer.get_correlation_matrix().round(2))
print("\nAUD-Gold Divergence Check:")
div = analyzer.check_divergence('AUD', 'GOLD')
if div:
print(f" Expected: {div['expected_correlation']:.2f}")
print(f" Actual: {div['actual_correlation']:.2f}")
print(f" Status: {div['divergence']}")
class BondCurrencyAnalyzer:
"""Analyze bond yields and currency relationships."""
def __init__(self):
self.yields: Dict[str, pd.Series] = {}
self.currencies: Dict[str, pd.Series] = {}
def add_yield(self, country: str, yields: pd.Series) -> None:
"""Add bond yield data."""
self.yields[country] = yields
def add_currency(self, pair: str, prices: pd.Series) -> None:
"""Add currency pair data."""
self.currencies[pair] = prices
def yield_spread(self, country1: str, country2: str) -> pd.Series:
"""Calculate yield spread between two countries."""
if country1 in self.yields and country2 in self.yields:
return self.yields[country1] - self.yields[country2]
return pd.Series()
def spread_correlation(self, country1: str, country2: str,
pair: str, window: int = 20) -> float:
"""Calculate correlation between yield spread and currency pair."""
spread = self.yield_spread(country1, country2)
currency = self.currencies.get(pair, pd.Series())
if spread.empty or currency.empty:
return 0.0
aligned = pd.concat([spread, currency], axis=1).dropna()
if len(aligned) < window:
return 0.0
return aligned.iloc[:, 0].corr(aligned.iloc[:, 1])
def analyze_carry_opportunity(self, high_yield: str, low_yield: str,
pair: str) -> Dict:
"""Analyze carry trade opportunity."""
if high_yield not in self.yields or low_yield not in self.yields:
return {}
spread = self.yield_spread(high_yield, low_yield)
current_spread = spread.iloc[-1] if len(spread) > 0 else 0
avg_spread = spread.mean() if len(spread) > 0 else 0
# Check if spread is widening or narrowing
recent_change = spread.iloc[-1] - spread.iloc[-5] if len(spread) >= 5 else 0
return {
'pair': pair,
'high_yield_country': high_yield,
'low_yield_country': low_yield,
'current_spread': current_spread,
'average_spread': avg_spread,
'spread_vs_avg': current_spread - avg_spread,
'recent_trend': 'widening' if recent_change > 0 else 'narrowing',
'carry_attractive': current_spread > avg_spread and recent_change > 0
}
# Demo
bond_analyzer = BondCurrencyAnalyzer()
# Simulate yield data
dates = pd.date_range('2024-01-01', periods=60, freq='D')
us_yields = pd.Series([4.5 + np.random.normal(0, 0.05) for _ in range(60)], index=dates)
jp_yields = pd.Series([0.1 + np.random.normal(0, 0.02) for _ in range(60)], index=dates)
eu_yields = pd.Series([3.0 + np.random.normal(0, 0.04) for _ in range(60)], index=dates)
bond_analyzer.add_yield('US', us_yields)
bond_analyzer.add_yield('JP', jp_yields)
bond_analyzer.add_yield('EU', eu_yields)
# Currency data
usdjpy = pd.Series([150 + np.random.normal(0, 0.5) for _ in range(60)], index=dates)
bond_analyzer.add_currency('USDJPY', usdjpy)
print("US-JP Yield Spread Analysis:")
spread = bond_analyzer.yield_spread('US', 'JP')
print(f" Current Spread: {spread.iloc[-1]:.2f}%")
print("\nCarry Trade Analysis (Long USD/JPY):")
carry = bond_analyzer.analyze_carry_opportunity('US', 'JP', 'USDJPY')
print(f" Spread vs Average: {carry['spread_vs_avg']:.2f}%")
print(f" Recent Trend: {carry['recent_trend']}")
print(f" Carry Attractive: {carry['carry_attractive']}")
Exercises
Exercise 1: Economic Indicator Analyzer (Guided)
Complete the IndicatorAnalyzer class that tracks economic indicators and generates currency bias.
class IndicatorAnalyzer:
"""Analyze economic indicators for currency bias."""
def __init__(self, country: str):
self.country = country
self.indicators: Dict[str, List[Dict]] = {}
def add_reading(self, indicator: str, actual: float,
forecast: float, previous: float) -> None:
"""Add an indicator reading."""
if indicator not in self.indicators:
self.indicators[indicator] = []
surprise = actual - forecast
trend = 'improving' if actual > previous else 'deteriorating'
self.indicators[indicator].append({
'actual': actual,
'forecast': forecast,
'previous': previous,
'surprise': surprise,
'beat': actual ______ forecast,
'trend': trend
})
def get_beat_rate(self, indicator: str) -> float:
"""Calculate what percentage of releases beat forecast."""
if indicator not in self.indicators:
return 0.0
readings = self.indicators[indicator]
beats = sum(1 for r in readings if r['beat'])
return (beats / ______(readings)) * 100 if readings else 0.0
def overall_bias(self) -> str:
"""Calculate overall currency bias from all indicators."""
if not self.indicators:
return 'neutral'
total_beats = 0
total_readings = 0
for readings in self.indicators.values():
if readings:
latest = readings[-1]
if latest['beat']:
total_beats += 1
total_readings += 1
if total_readings == 0:
return 'neutral'
beat_pct = total_beats / total_readings
if beat_pct >= ______:
return 'bullish'
elif beat_pct <= 0.3:
return 'bearish'
return 'neutral'
# Test
analyzer = IndicatorAnalyzer('US')
analyzer.add_reading('NFP', 303, 212, 275) # Beat
analyzer.add_reading('CPI', 3.5, 3.4, 3.2) # Beat
analyzer.add_reading('GDP', 1.6, 2.5, 3.4) # Miss
print(f"NFP Beat Rate: {analyzer.get_beat_rate('NFP'):.1f}%")
print(f"Overall Bias: {analyzer.overall_bias()}")
Solution 1
class IndicatorAnalyzer:
"""Analyze economic indicators for currency bias."""
def __init__(self, country: str):
self.country = country
self.indicators: Dict[str, List[Dict]] = {}
def add_reading(self, indicator: str, actual: float,
forecast: float, previous: float) -> None:
"""Add an indicator reading."""
if indicator not in self.indicators:
self.indicators[indicator] = []
surprise = actual - forecast
trend = 'improving' if actual > previous else 'deteriorating'
self.indicators[indicator].append({
'actual': actual,
'forecast': forecast,
'previous': previous,
'surprise': surprise,
'beat': actual > forecast,
'trend': trend
})
def get_beat_rate(self, indicator: str) -> float:
"""Calculate what percentage of releases beat forecast."""
if indicator not in self.indicators:
return 0.0
readings = self.indicators[indicator]
beats = sum(1 for r in readings if r['beat'])
return (beats / len(readings)) * 100 if readings else 0.0
def overall_bias(self) -> str:
"""Calculate overall currency bias from all indicators."""
if not self.indicators:
return 'neutral'
total_beats = 0
total_readings = 0
for readings in self.indicators.values():
if readings:
latest = readings[-1]
if latest['beat']:
total_beats += 1
total_readings += 1
if total_readings == 0:
return 'neutral'
beat_pct = total_beats / total_readings
if beat_pct >= 0.6:
return 'bullish'
elif beat_pct <= 0.3:
return 'bearish'
return 'neutral'
Exercise 2: Central Bank Rate Tracker (Guided)
Complete the RateTracker class that monitors central bank rates and calculates rate differentials.
class RateTracker:
"""Track central bank interest rates."""
def __init__(self):
self.rates: Dict[str, float] = {}
self.history: Dict[str, List[Tuple[datetime, float]]] = {}
def set_rate(self, currency: str, rate: float, date: datetime = None) -> None:
"""Set current rate for a currency."""
self.rates[currency] = rate
if currency not in self.history:
self.history[currency] = []
self.history[currency].______((
date or datetime.now(),
rate
))
def get_differential(self, curr1: str, curr2: str) -> float:
"""Get rate differential between two currencies."""
rate1 = self.rates.get(curr1, 0)
rate2 = self.rates.get(curr2, 0)
return rate1 ______ rate2
def get_highest_yielding(self) -> Tuple[str, float]:
"""Get currency with highest yield."""
if not self.rates:
return ('', 0.0)
return ______(self.rates.items(), key=lambda x: x[1])
def get_carry_pairs(self) -> List[Dict]:
"""Get best carry trade pairs (long high yield, short low yield)."""
pairs = []
currencies = list(self.rates.keys())
for i, c1 in enumerate(currencies):
for c2 in currencies[i+1:]:
diff = self.get_differential(c1, c2)
if abs(diff) >= 1.0: # Minimum 1% differential
pairs.append({
'long': c1 if diff > 0 else c2,
'short': c2 if diff > 0 else c1,
'differential': abs(diff)
})
return sorted(pairs, key=lambda x: x['differential'], reverse=True)
# Test
tracker = RateTracker()
tracker.set_rate('USD', 5.25)
tracker.set_rate('EUR', 4.50)
tracker.set_rate('JPY', 0.10)
tracker.set_rate('GBP', 5.00)
print(f"USD-JPY Differential: {tracker.get_differential('USD', 'JPY'):.2f}%")
print(f"Highest Yielding: {tracker.get_highest_yielding()}")
print(f"\nBest Carry Pairs:")
for pair in tracker.get_carry_pairs()[:3]:
print(f" Long {pair['long']}/Short {pair['short']}: {pair['differential']:.2f}%")
Solution 2
class RateTracker:
"""Track central bank interest rates."""
def __init__(self):
self.rates: Dict[str, float] = {}
self.history: Dict[str, List[Tuple[datetime, float]]] = {}
def set_rate(self, currency: str, rate: float, date: datetime = None) -> None:
"""Set current rate for a currency."""
self.rates[currency] = rate
if currency not in self.history:
self.history[currency] = []
self.history[currency].append((
date or datetime.now(),
rate
))
def get_differential(self, curr1: str, curr2: str) -> float:
"""Get rate differential between two currencies."""
rate1 = self.rates.get(curr1, 0)
rate2 = self.rates.get(curr2, 0)
return rate1 - rate2
def get_highest_yielding(self) -> Tuple[str, float]:
"""Get currency with highest yield."""
if not self.rates:
return ('', 0.0)
return max(self.rates.items(), key=lambda x: x[1])
def get_carry_pairs(self) -> List[Dict]:
"""Get best carry trade pairs."""
pairs = []
currencies = list(self.rates.keys())
for i, c1 in enumerate(currencies):
for c2 in currencies[i+1:]:
diff = self.get_differential(c1, c2)
if abs(diff) >= 1.0:
pairs.append({
'long': c1 if diff > 0 else c2,
'short': c2 if diff > 0 else c1,
'differential': abs(diff)
})
return sorted(pairs, key=lambda x: x['differential'], reverse=True)
Exercise 3: Event Impact Scorer (Guided)
Complete the EventScorer class that scores economic events based on their potential market impact.
class EventScorer:
"""Score economic events for trading decisions."""
BASE_SCORES = {
'rate_decision': 10,
'nfp': 9,
'cpi': 8,
'gdp': 7,
'retail_sales': 5,
'pmi': 4
}
def __init__(self):
self.events: List[Dict] = []
def add_event(self, event_type: str, currency: str,
surprise_pct: float = 0) -> None:
"""Add event with its surprise magnitude."""
base_score = self.BASE_SCORES.get(event_type.lower(), 3)
# Adjust score based on surprise magnitude
surprise_multiplier = 1 + (______(surprise_pct) / 100)
final_score = base_score * surprise_multiplier
self.events.append({
'type': event_type,
'currency': currency,
'surprise_pct': surprise_pct,
'score': final_score,
'direction': 'bullish' if surprise_pct > 0 else ('bearish' if surprise_pct < 0 else 'neutral')
})
def get_currency_score(self, currency: str) -> Dict:
"""Get aggregate score for a currency."""
currency_events = [e for e in self.events if e['currency'] == currency]
if not currency_events:
return {'currency': currency, 'score': 0, 'bias': 'neutral'}
bullish_score = ______(e['score'] for e in currency_events if e['direction'] == 'bullish')
bearish_score = sum(e['score'] for e in currency_events if e['direction'] == 'bearish')
net_score = bullish_score - bearish_score
return {
'currency': currency,
'bullish_score': bullish_score,
'bearish_score': bearish_score,
'net_score': net_score,
'bias': 'bullish' if net_score > 5 else ('______' if net_score < -5 else 'neutral')
}
# Test
scorer = EventScorer()
scorer.add_event('NFP', 'USD', 15.5) # Big beat
scorer.add_event('CPI', 'USD', 2.9) # Small beat
scorer.add_event('GDP', 'USD', -36.0) # Big miss
result = scorer.get_currency_score('USD')
print(f"USD Analysis:")
print(f" Bullish Score: {result['bullish_score']:.1f}")
print(f" Bearish Score: {result['bearish_score']:.1f}")
print(f" Net Score: {result['net_score']:.1f}")
print(f" Bias: {result['bias']}")
Solution 3
class EventScorer:
"""Score economic events for trading decisions."""
BASE_SCORES = {
'rate_decision': 10,
'nfp': 9,
'cpi': 8,
'gdp': 7,
'retail_sales': 5,
'pmi': 4
}
def __init__(self):
self.events: List[Dict] = []
def add_event(self, event_type: str, currency: str,
surprise_pct: float = 0) -> None:
"""Add event with its surprise magnitude."""
base_score = self.BASE_SCORES.get(event_type.lower(), 3)
surprise_multiplier = 1 + (abs(surprise_pct) / 100)
final_score = base_score * surprise_multiplier
self.events.append({
'type': event_type,
'currency': currency,
'surprise_pct': surprise_pct,
'score': final_score,
'direction': 'bullish' if surprise_pct > 0 else ('bearish' if surprise_pct < 0 else 'neutral')
})
def get_currency_score(self, currency: str) -> Dict:
"""Get aggregate score for a currency."""
currency_events = [e for e in self.events if e['currency'] == currency]
if not currency_events:
return {'currency': currency, 'score': 0, 'bias': 'neutral'}
bullish_score = sum(e['score'] for e in currency_events if e['direction'] == 'bullish')
bearish_score = sum(e['score'] for e in currency_events if e['direction'] == 'bearish')
net_score = bullish_score - bearish_score
return {
'currency': currency,
'bullish_score': bullish_score,
'bearish_score': bearish_score,
'net_score': net_score,
'bias': 'bullish' if net_score > 5 else ('bearish' if net_score < -5 else 'neutral')
}
Exercise 4: Complete Economic Calendar (Open-ended)
Build a comprehensive economic calendar system that: - Stores events with full details - Filters by date range, country, and impact - Calculates risk scores for trading windows - Generates pre-event alerts
# Exercise 4: Complete Economic Calendar (Open-ended)
#
# Requirements:
# 1. Create class ComprehensiveCalendar
# 2. Store events with: name, country, datetime, impact, forecast, previous, actual
# 3. Methods: add_event, get_events_in_range, filter_by_impact, filter_by_country
# 4. Calculate risk_score for a time window (sum of impact levels)
# 5. Generate alerts for high-impact events in next N hours
#
# Your implementation:
Solution 4
class ComprehensiveCalendar:
"""Full-featured economic calendar."""
def __init__(self):
self.events: List[Dict] = []
def add_event(self, name: str, country: str, dt: datetime,
impact: int, forecast: float = None,
previous: float = None, actual: float = None) -> None:
"""Add event to calendar."""
self.events.append({
'name': name,
'country': country,
'datetime': dt,
'impact': impact, # 1-3
'forecast': forecast,
'previous': previous,
'actual': actual
})
self.events.sort(key=lambda x: x['datetime'])
def get_events_in_range(self, start: datetime, end: datetime) -> List[Dict]:
"""Get events within date range."""
return [e for e in self.events if start <= e['datetime'] <= end]
def filter_by_impact(self, min_impact: int = 2) -> List[Dict]:
"""Filter events by minimum impact level."""
return [e for e in self.events if e['impact'] >= min_impact]
def filter_by_country(self, country: str) -> List[Dict]:
"""Filter events by country."""
return [e for e in self.events if e['country'] == country]
def risk_score(self, start: datetime, end: datetime) -> int:
"""Calculate risk score for time window."""
events = self.get_events_in_range(start, end)
return sum(e['impact'] for e in events)
def get_alerts(self, hours_ahead: int = 24) -> List[Dict]:
"""Get alerts for high-impact events."""
now = datetime.now()
cutoff = now + timedelta(hours=hours_ahead)
high_impact = [
e for e in self.events
if now <= e['datetime'] <= cutoff and e['impact'] >= 3
]
return [{
'event': e['name'],
'country': e['country'],
'time': e['datetime'],
'hours_until': (e['datetime'] - now).total_seconds() / 3600
} for e in high_impact]
Exercise 5: Intermarket Correlation Tracker (Open-ended)
Build an intermarket analyzer that: - Tracks correlations between currencies and related assets - Detects correlation breakdowns - Generates divergence signals
# Exercise 5: Intermarket Correlation Tracker (Open-ended)
#
# Requirements:
# 1. Create class CorrelationTracker
# 2. Store price series for multiple assets
# 3. Calculate rolling correlations
# 4. Define expected correlations (e.g., AUD-Gold positive)
# 5. Detect when actual correlation deviates from expected
# 6. Generate trading signals from divergences
#
# Your implementation:
Solution 5
class CorrelationTracker:
"""Track and analyze intermarket correlations."""
EXPECTED = {
('AUD', 'GOLD'): 0.6,
('CAD', 'OIL'): 0.5,
('USD', 'GOLD'): -0.4,
('JPY', 'VIX'): 0.5,
}
def __init__(self):
self.data: Dict[str, pd.Series] = {}
def add_series(self, symbol: str, prices: pd.Series) -> None:
"""Add price series."""
self.data[symbol] = prices
def rolling_correlation(self, sym1: str, sym2: str, window: int = 20) -> pd.Series:
"""Calculate rolling correlation."""
if sym1 not in self.data or sym2 not in self.data:
return pd.Series()
r1 = self.data[sym1].pct_change()
r2 = self.data[sym2].pct_change()
return r1.rolling(window).corr(r2)
def check_divergence(self, sym1: str, sym2: str, threshold: float = 0.3) -> Dict:
"""Check for correlation divergence."""
expected = self.EXPECTED.get((sym1, sym2), 0)
if expected == 0:
expected = self.EXPECTED.get((sym2, sym1), 0)
actual = self.rolling_correlation(sym1, sym2).iloc[-1]
deviation = actual - expected
return {
'pair': f"{sym1}-{sym2}",
'expected': expected,
'actual': actual,
'deviation': deviation,
'divergence': abs(deviation) > threshold,
'signal': 'divergence_detected' if abs(deviation) > threshold else 'normal'
}
def scan_all_pairs(self) -> List[Dict]:
"""Scan all known pairs for divergences."""
signals = []
for (sym1, sym2) in self.EXPECTED.keys():
if sym1 in self.data and sym2 in self.data:
result = self.check_divergence(sym1, sym2)
if result['divergence']:
signals.append(result)
return signals
Exercise 6: Fundamental Dashboard Builder (Open-ended)
Build a comprehensive fundamental analysis dashboard that combines: - Economic indicators tracking - Central bank monitoring - Economic calendar - Intermarket analysis - Overall currency bias scoring
# Exercise 6: Fundamental Dashboard Builder (Open-ended)
#
# Requirements:
# 1. Create class FundamentalDashboard
# 2. Integrate: indicator tracker, central bank monitor, calendar, intermarket
# 3. Calculate overall fundamental score per currency
# 4. Rank currencies from most bullish to most bearish
# 5. Generate pair recommendations (long strongest vs short weakest)
# 6. Print formatted dashboard report
#
# Your implementation:
Solution 6
class FundamentalDashboard:
"""Comprehensive fundamental analysis dashboard."""
def __init__(self):
self.indicators: Dict[str, EconomicIndicatorTracker] = {}
self.central_banks: Dict[str, CentralBank] = {}
self.calendar = EconomicCalendar()
self.intermarket = IntermarketAnalyzer()
def add_country(self, country: str, currency: str, rate: float) -> None:
"""Add a country to track."""
self.indicators[currency] = EconomicIndicatorTracker(country)
self.central_banks[currency] = CentralBank(f"{country} CB", currency, rate)
def calculate_currency_score(self, currency: str) -> Dict:
"""Calculate overall fundamental score for currency."""
score = 50 # Neutral base
factors = []
# Economic indicators
if currency in self.indicators:
econ_score = self.indicators[currency].get_economic_score()
score += (econ_score['overall_score'] - 50) * 0.4
factors.append(f"Econ: {econ_score['interpretation']}")
# Central bank stance
if currency in self.central_banks:
stance = self.central_banks[currency].get_policy_stance()
if stance == 'hawkish':
score += 15
elif stance == 'dovish':
score -= 15
factors.append(f"CB: {stance}")
# Event risk
risk = self.calendar.currency_risk_score(currency)
if risk['recommendation'] == 'avoid':
factors.append("High event risk")
return {
'currency': currency,
'score': max(0, min(100, score)),
'bias': 'bullish' if score > 60 else ('bearish' if score < 40 else 'neutral'),
'factors': factors
}
def rank_currencies(self) -> List[Dict]:
"""Rank all currencies by fundamental score."""
rankings = []
for currency in self.central_banks.keys():
rankings.append(self.calculate_currency_score(currency))
return sorted(rankings, key=lambda x: x['score'], reverse=True)
def get_pair_recommendations(self) -> List[Dict]:
"""Get pair recommendations based on rankings."""
rankings = self.rank_currencies()
if len(rankings) < 2:
return []
recommendations = []
strongest = rankings[0]
weakest = rankings[-1]
if strongest['score'] - weakest['score'] > 20:
recommendations.append({
'long': strongest['currency'],
'short': weakest['currency'],
'score_diff': strongest['score'] - weakest['score'],
'confidence': 'high' if strongest['score'] - weakest['score'] > 30 else 'medium'
})
return recommendations
def print_report(self) -> None:
"""Print formatted dashboard report."""
print("=" * 50)
print(" FUNDAMENTAL ANALYSIS DASHBOARD")
print("=" * 50)
print("\nCurrency Rankings:")
for r in self.rank_currencies():
print(f" {r['currency']}: {r['score']:.0f} ({r['bias']})")
print(f" Factors: {', '.join(r['factors'])}")
print("\nRecommendations:")
for rec in self.get_pair_recommendations():
print(f" Long {rec['long']}/Short {rec['short']}")
print(f" Confidence: {rec['confidence']}")
Module Project: Fundamental Analysis Dashboard
Build a production-ready fundamental analysis system that integrates all concepts from this module.
class FundamentalAnalysisDashboard:
"""
Production-ready fundamental analysis dashboard.
Integrates: Economic indicators, Central banks, Calendar, Intermarket.
Provides: Currency rankings, Pair recommendations, Risk assessment.
"""
CURRENCIES = ['USD', 'EUR', 'GBP', 'JPY', 'AUD', 'CAD', 'CHF', 'NZD']
def __init__(self):
self.indicator_trackers: Dict[str, EconomicIndicatorTracker] = {}
self.central_banks: Dict[str, CentralBank] = {}
self.calendar = EconomicCalendar()
self.intermarket = IntermarketAnalyzer()
self.currency_scores: Dict[str, float] = {}
def initialize_country(self, currency: str, country: str,
current_rate: float, cb_name: str) -> None:
"""Initialize tracking for a country/currency."""
self.indicator_trackers[currency] = EconomicIndicatorTracker(country)
self.central_banks[currency] = CentralBank(cb_name, currency, current_rate)
def update_indicator(self, currency: str, indicator: str,
actual: float, forecast: float, previous: float) -> None:
"""Update an economic indicator."""
if currency in self.indicator_trackers:
self.indicator_trackers[currency].update_indicator(
indicator, datetime.now(), actual, forecast, previous
)
def update_rate_decision(self, currency: str, new_rate: float,
expected: float, tone: str = 'neutral') -> None:
"""Record a rate decision."""
if currency in self.central_banks:
self.central_banks[currency].add_rate_decision(
datetime.now(), new_rate, expected, tone
)
def calculate_fundamental_score(self, currency: str) -> Dict:
"""Calculate comprehensive fundamental score."""
score = 50 # Neutral base
components = {}
# 1. Economic Indicators (40% weight)
if currency in self.indicator_trackers:
econ = self.indicator_trackers[currency].get_economic_score()
econ_score = econ['overall_score']
score += (econ_score - 50) * 0.4
components['economic'] = econ_score
# 2. Central Bank Stance (30% weight)
if currency in self.central_banks:
cb = self.central_banks[currency]
stance = cb.get_policy_stance()
stance_score = 70 if stance == 'hawkish' else (30 if stance == 'dovish' else 50)
score += (stance_score - 50) * 0.3
components['central_bank'] = {
'rate': cb.current_rate,
'stance': stance,
'score': stance_score
}
# 3. Rate Differentials (20% weight)
if len(self.central_banks) > 1:
avg_rate = np.mean([cb.current_rate for cb in self.central_banks.values()])
if currency in self.central_banks:
diff = self.central_banks[currency].current_rate - avg_rate
diff_score = 50 + diff * 10 # +10 points per 1% above average
score += (diff_score - 50) * 0.2
components['rate_differential'] = diff
# 4. Event Risk (10% weight - penalty only)
risk = self.calendar.currency_risk_score(currency)
if risk['risk_score'] >= 5:
score -= 5
components['event_risk'] = 'high'
else:
components['event_risk'] = 'normal'
final_score = max(0, min(100, score))
self.currency_scores[currency] = final_score
return {
'currency': currency,
'score': final_score,
'bias': self._score_to_bias(final_score),
'components': components
}
def _score_to_bias(self, score: float) -> str:
"""Convert score to bias label."""
if score >= 65:
return 'strong_bullish'
elif score >= 55:
return 'bullish'
elif score >= 45:
return 'neutral'
elif score >= 35:
return 'bearish'
return 'strong_bearish'
def get_currency_rankings(self) -> List[Dict]:
"""Rank all currencies by fundamental score."""
rankings = []
for currency in self.central_banks.keys():
rankings.append(self.calculate_fundamental_score(currency))
return sorted(rankings, key=lambda x: x['score'], reverse=True)
def get_trade_recommendations(self) -> List[Dict]:
"""Generate trade recommendations."""
rankings = self.get_currency_rankings()
recommendations = []
if len(rankings) < 2:
return recommendations
# Top recommendation: strongest vs weakest
strongest = rankings[0]
weakest = rankings[-1]
score_diff = strongest['score'] - weakest['score']
if score_diff >= 15:
confidence = 'high' if score_diff >= 30 else ('medium' if score_diff >= 20 else 'low')
recommendations.append({
'type': 'divergence',
'long': strongest['currency'],
'short': weakest['currency'],
'long_score': strongest['score'],
'short_score': weakest['score'],
'score_differential': score_diff,
'confidence': confidence,
'rationale': f"{strongest['currency']} {strongest['bias']} vs {weakest['currency']} {weakest['bias']}"
})
# Secondary: second strongest vs second weakest
if len(rankings) >= 4:
second_strong = rankings[1]
second_weak = rankings[-2]
diff2 = second_strong['score'] - second_weak['score']
if diff2 >= 15:
recommendations.append({
'type': 'secondary',
'long': second_strong['currency'],
'short': second_weak['currency'],
'long_score': second_strong['score'],
'short_score': second_weak['score'],
'score_differential': diff2,
'confidence': 'medium' if diff2 >= 20 else 'low',
'rationale': f"Secondary divergence play"
})
return recommendations
def print_dashboard(self) -> None:
"""Print formatted dashboard."""
print("\n" + "=" * 60)
print(" FUNDAMENTAL ANALYSIS DASHBOARD")
print(f" {datetime.now().strftime('%Y-%m-%d %H:%M')}")
print("=" * 60)
# Currency Rankings
print("\n" + "-" * 40)
print("CURRENCY RANKINGS")
print("-" * 40)
for rank, data in enumerate(self.get_currency_rankings(), 1):
bar = '█' * int(data['score'] / 10)
print(f"{rank}. {data['currency']}: {data['score']:.0f} {bar}")
print(f" Bias: {data['bias']}")
if 'central_bank' in data['components']:
cb = data['components']['central_bank']
print(f" Rate: {cb['rate']:.2f}% | Stance: {cb['stance']}")
# Trade Recommendations
print("\n" + "-" * 40)
print("TRADE RECOMMENDATIONS")
print("-" * 40)
recommendations = self.get_trade_recommendations()
if recommendations:
for i, rec in enumerate(recommendations, 1):
print(f"\n{i}. Long {rec['long']} / Short {rec['short']}")
print(f" Scores: {rec['long_score']:.0f} vs {rec['short_score']:.0f}")
print(f" Differential: {rec['score_differential']:.0f} points")
print(f" Confidence: {rec['confidence'].upper()}")
print(f" Rationale: {rec['rationale']}")
else:
print("\nNo clear opportunities - markets balanced")
# Upcoming Events
print("\n" + "-" * 40)
print("HIGH-IMPACT EVENTS (24H)")
print("-" * 40)
high_impact = self.calendar.get_upcoming(24, EventImpact.HIGH)
if high_impact:
for event in high_impact[:5]:
print(f" {event.datetime.strftime('%H:%M')} - {event.country}: {event.name}")
else:
print(" No high-impact events scheduled")
print("\n" + "=" * 60)
# Demo the dashboard
dashboard = FundamentalAnalysisDashboard()
# Initialize major currencies
dashboard.initialize_country('USD', 'US', 5.25, 'Federal Reserve')
dashboard.initialize_country('EUR', 'EU', 4.50, 'ECB')
dashboard.initialize_country('GBP', 'UK', 5.00, 'Bank of England')
dashboard.initialize_country('JPY', 'JP', 0.10, 'Bank of Japan')
dashboard.initialize_country('AUD', 'AU', 4.35, 'RBA')
dashboard.initialize_country('CAD', 'CA', 5.00, 'Bank of Canada')
# Add some indicator data
dashboard.update_indicator('USD', 'Non-Farm Payrolls', 303, 212, 275)
dashboard.update_indicator('USD', 'CPI YoY', 3.5, 3.4, 3.2)
dashboard.update_indicator('EUR', 'CPI YoY', 2.4, 2.5, 2.6)
dashboard.update_indicator('GBP', 'CPI YoY', 3.2, 3.4, 4.0)
dashboard.update_indicator('JPY', 'CPI YoY', 2.8, 2.6, 2.5)
# Add rate decisions
dashboard.update_rate_decision('USD', 5.25, 5.25, 'hawkish')
dashboard.update_rate_decision('EUR', 4.50, 4.50, 'dovish')
dashboard.update_rate_decision('JPY', 0.10, 0.10, 'dovish')
# Add calendar events
base_time = datetime.now() + timedelta(hours=2)
dashboard.calendar.add_event(CalendarEvent(
'FOMC Minutes', 'US', base_time, EventImpact.HIGH
))
dashboard.calendar.add_event(CalendarEvent(
'ECB Speech', 'EU', base_time + timedelta(hours=3), EventImpact.MEDIUM
))
# Print the dashboard
dashboard.print_dashboard()
Key Takeaways
- Economic Indicators: Track GDP, inflation, employment to gauge economic health and currency direction
- Central Banks: Monitor rate decisions and policy stance (hawkish vs dovish) for major currency moves
- Rate Differentials: Higher interest rates attract capital, supporting currency strength
- Economic Calendar: Schedule trading around high-impact events; avoid unexpected volatility
- Intermarket Analysis: Currencies correlate with commodities (AUD-Gold, CAD-Oil) and bonds
- Carry Trade: Long high-yield, short low-yield currencies when conditions favor risk-on
- Divergence: Policy divergence between central banks creates trending opportunities
- Combine Analysis: Best results come from aligning fundamental bias with technical setups
Next: Module 7 - Commodity Trading where we'll explore gold, oil, and agricultural markets with their currency relationships.
Module 7: Commodity Trading
Part 2: Analysis & Strategies
| Duration | Exercises |
|---|---|
| ~2.5 hours | 6 |
Learning Objectives
- Understand gold market dynamics and safe-haven trading
- Analyze crude oil supply/demand and inventory data
- Trade agricultural commodities with seasonal patterns
- Exploit commodity currency correlations (AUD, CAD, NOK)
Prerequisites
- Modules 1-6 (Forex/Futures fundamentals, technical & fundamental analysis)
- Understanding of correlation analysis
- Python pandas proficiency
import pandas as pd
import numpy as np
from datetime import datetime, timedelta
from dataclasses import dataclass, field
from typing import List, Dict, Tuple, Optional
from enum import Enum
import warnings
warnings.filterwarnings('ignore')
7.1 Gold Trading
Gold (XAU) is the premier safe-haven asset, with unique market dynamics driven by fear, inflation expectations, and USD strength.
Gold Market Fundamentals
| Factor | Impact on Gold | Explanation |
|---|---|---|
| USD Strength | Negative | Gold priced in USD; strong USD = cheaper gold |
| Real Interest Rates | Negative | Higher real yields = opportunity cost of holding gold |
| Inflation Expectations | Positive | Gold as inflation hedge |
| Geopolitical Risk | Positive | Safe-haven demand |
| Central Bank Buying | Positive | Reserve diversification |
class GoldAnalyzer:
"""Analyze gold market dynamics and generate trading signals."""
def __init__(self):
self.gold_prices: pd.Series = pd.Series(dtype=float)
self.dxy_prices: pd.Series = pd.Series() # USD Index
self.real_yields: pd.Series = pd.Series() # 10Y TIPS yield
self.vix: pd.Series = pd.Series() # Fear index
def set_data(self, gold: pd.Series, dxy: pd.Series = None,
real_yields: pd.Series = None, vix: pd.Series = None) -> None:
"""Set market data."""
self.gold_prices = gold
if dxy is not None:
self.dxy_prices = dxy
if real_yields is not None:
self.real_yields = real_yields
if vix is not None:
self.vix = vix
def calculate_gold_usd_correlation(self, window: int = 20) -> pd.Series:
"""Calculate rolling correlation between gold and USD."""
if self.dxy_prices.empty:
return pd.Series(dtype=float)
gold_ret = self.gold_prices.pct_change()
dxy_ret = self.dxy_prices.pct_change()
return gold_ret.rolling(window).corr(dxy_ret)
def safe_haven_signal(self, vix_threshold: float = 25) -> Dict:
"""Generate safe-haven demand signal."""
if self.vix.empty:
return {'signal': 'no_data'}
current_vix = self.vix.iloc[-1]
vix_change = self.vix.iloc[-1] - self.vix.iloc[-5] if len(self.vix) >= 5 else 0
if current_vix > vix_threshold and vix_change > 5:
signal = 'strong_buy'
reason = 'Fear spike - safe haven demand'
elif current_vix > vix_threshold:
signal = 'buy'
reason = 'Elevated fear - gold supportive'
elif current_vix < 15 and vix_change < -3:
signal = 'sell'
reason = 'Risk-on environment - gold headwind'
else:
signal = 'neutral'
reason = 'Normal volatility'
return {
'signal': signal,
'vix': current_vix,
'vix_change': vix_change,
'reason': reason
}
def real_yield_signal(self) -> Dict:
"""Generate signal based on real yields."""
if self.real_yields.empty:
return {'signal': 'no_data'}
current_yield = self.real_yields.iloc[-1]
yield_ma = self.real_yields.rolling(20).mean().iloc[-1]
# Gold is negative correlated with real yields
if current_yield < yield_ma and current_yield < 0:
signal = 'buy'
reason = 'Negative real yields support gold'
elif current_yield > yield_ma and current_yield > 1.5:
signal = 'sell'
reason = 'High real yields headwind for gold'
else:
signal = 'neutral'
reason = 'Real yields neutral'
return {
'signal': signal,
'real_yield': current_yield,
'yield_ma': yield_ma,
'reason': reason
}
def combined_signal(self) -> Dict:
"""Combine all factors for overall gold signal."""
signals = {
'safe_haven': self.safe_haven_signal(),
'real_yield': self.real_yield_signal()
}
# Score each signal
score = 0
signal_map = {'strong_buy': 2, 'buy': 1, 'neutral': 0, 'sell': -1, 'strong_sell': -2}
for name, sig in signals.items():
if sig['signal'] in signal_map:
score += signal_map[sig['signal']]
if score >= 2:
overall = 'strong_buy'
elif score >= 1:
overall = 'buy'
elif score <= -2:
overall = 'strong_sell'
elif score <= -1:
overall = 'sell'
else:
overall = 'neutral'
return {
'overall': overall,
'score': score,
'components': signals
}
# Demo
np.random.seed(42)
dates = pd.date_range('2024-01-01', periods=60, freq='D')
gold_analyzer = GoldAnalyzer()
gold_analyzer.set_data(
gold=pd.Series(2000 + np.cumsum(np.random.normal(0, 15, 60)), index=dates),
dxy=pd.Series(104 + np.cumsum(np.random.normal(0, 0.3, 60)), index=dates),
vix=pd.Series(np.clip(18 + np.cumsum(np.random.normal(0, 1, 60)), 10, 40), index=dates),
real_yields=pd.Series(0.5 + np.cumsum(np.random.normal(0, 0.05, 60)), index=dates)
)
print("Gold Analysis:")
print(f"\nSafe Haven Signal: {gold_analyzer.safe_haven_signal()}")
print(f"\nReal Yield Signal: {gold_analyzer.real_yield_signal()}")
print(f"\nCombined Signal: {gold_analyzer.combined_signal()['overall']}")
7.2 Oil Trading
Crude oil (WTI/Brent) is driven by supply/demand dynamics, OPEC decisions, and inventory data.
Key Oil Market Drivers
| Factor | Impact | Data Source |
|---|---|---|
| EIA Inventory | Build = Bearish, Draw = Bullish | Weekly (Wed) |
| OPEC Production | Cuts = Bullish, Increases = Bearish | Monthly |
| Global Demand | GDP growth correlation | IMF, EIA |
| Geopolitical | Supply disruption = Bullish | News |
| USD Strength | Negative correlation | DXY |
class OilAnalyzer:
"""Analyze crude oil market and inventory data."""
def __init__(self):
self.prices: pd.Series = pd.Series(dtype=float)
self.inventory_data: List[Dict] = []
self.opec_production: List[Dict] = []
def set_prices(self, prices: pd.Series) -> None:
"""Set oil price data."""
self.prices = prices
def add_inventory_report(self, date: datetime, actual: float,
forecast: float, previous: float) -> None:
"""Add EIA inventory report (in millions of barrels)."""
change = actual - previous
surprise = actual - forecast
self.inventory_data.append({
'date': date,
'actual': actual,
'forecast': forecast,
'previous': previous,
'change': change,
'surprise': surprise,
'type': 'build' if change > 0 else 'draw'
})
def inventory_trend(self, periods: int = 4) -> Dict:
"""Analyze recent inventory trend."""
if len(self.inventory_data) < periods:
return {'trend': 'insufficient_data'}
recent = self.inventory_data[-periods:]
builds = sum(1 for r in recent if r['type'] == 'build')
draws = periods - builds
total_change = sum(r['change'] for r in recent)
avg_surprise = np.mean([r['surprise'] for r in recent])
if draws > builds:
trend = 'bullish'
elif builds > draws:
trend = 'bearish'
else:
trend = 'neutral'
return {
'trend': trend,
'builds': builds,
'draws': draws,
'total_change_mb': total_change,
'avg_surprise': avg_surprise
}
def inventory_signal(self) -> Dict:
"""Generate trading signal from latest inventory."""
if not self.inventory_data:
return {'signal': 'no_data'}
latest = self.inventory_data[-1]
trend = self.inventory_trend()
# Signal based on surprise and trend
if latest['surprise'] < -3 and trend['trend'] == 'bullish':
signal = 'strong_buy'
reason = 'Large draw surprise + bullish trend'
elif latest['surprise'] < -1:
signal = 'buy'
reason = 'Draw larger than expected'
elif latest['surprise'] > 3 and trend['trend'] == 'bearish':
signal = 'strong_sell'
reason = 'Large build surprise + bearish trend'
elif latest['surprise'] > 1:
signal = 'sell'
reason = 'Build larger than expected'
else:
signal = 'neutral'
reason = 'Inventory in line with expectations'
return {
'signal': signal,
'reason': reason,
'latest_change': latest['change'],
'surprise': latest['surprise'],
'trend': trend['trend']
}
def calculate_seasonality(self) -> Dict:
"""Calculate typical seasonal patterns."""
if self.prices.empty:
return {}
# Group by month and calculate average returns
returns = self.prices.pct_change()
monthly_returns = returns.groupby(returns.index.month).mean() * 100
return {
'monthly_avg_returns': monthly_returns.to_dict(),
'best_month': monthly_returns.idxmax(),
'worst_month': monthly_returns.idxmin()
}
# Demo
oil_analyzer = OilAnalyzer()
oil_analyzer.set_prices(pd.Series(75 + np.cumsum(np.random.normal(0, 1, 60)), index=dates))
# Add inventory reports
oil_analyzer.add_inventory_report(datetime(2024, 4, 3), 451.2, 453.0, 452.5) # Draw
oil_analyzer.add_inventory_report(datetime(2024, 4, 10), 449.8, 451.0, 451.2) # Draw
oil_analyzer.add_inventory_report(datetime(2024, 4, 17), 448.5, 450.5, 449.8) # Draw
oil_analyzer.add_inventory_report(datetime(2024, 4, 24), 446.2, 449.0, 448.5) # Large draw
print("Oil Inventory Analysis:")
print(f"\nTrend: {oil_analyzer.inventory_trend()}")
print(f"\nSignal: {oil_analyzer.inventory_signal()}")
7.3 Agricultural Commodities
Agricultural commodities (grains, softs) have strong seasonal patterns driven by planting, growing, and harvest cycles.
Major Agricultural Markets
| Commodity | Symbol | Key Factors |
|---|---|---|
| Corn | ZC | US planting (Apr-May), Harvest (Sep-Nov) |
| Wheat | ZW | Winter/Spring varieties, Global weather |
| Soybeans | ZS | China demand, Brazil crop |
| Coffee | KC | Brazil frost, Colombian production |
| Sugar | SB | Brazil production, Ethanol demand |
class AgriculturalAnalyzer:
"""Analyze agricultural commodities with seasonal patterns."""
# Typical seasonal patterns (month -> expected direction)
SEASONAL_PATTERNS = {
'corn': {
1: 'neutral', 2: 'neutral', 3: 'bullish', # Pre-planting
4: 'bullish', 5: 'bullish', 6: 'bearish', # Planting uncertainty
7: 'volatile', 8: 'volatile', 9: 'bearish', # Weather/harvest
10: 'bearish', 11: 'bearish', 12: 'neutral' # Harvest pressure
},
'wheat': {
1: 'neutral', 2: 'neutral', 3: 'bullish',
4: 'bullish', 5: 'volatile', 6: 'bearish',
7: 'bearish', 8: 'neutral', 9: 'neutral',
10: 'neutral', 11: 'neutral', 12: 'neutral'
},
'soybeans': {
1: 'neutral', 2: 'bullish', 3: 'bullish',
4: 'bullish', 5: 'volatile', 6: 'volatile',
7: 'volatile', 8: 'bearish', 9: 'bearish',
10: 'bearish', 11: 'neutral', 12: 'neutral'
}
}
def __init__(self, commodity: str):
self.commodity = commodity.lower()
self.prices: pd.Series = pd.Series(dtype=float)
self.weather_events: List[Dict] = []
def set_prices(self, prices: pd.Series) -> None:
"""Set price data."""
self.prices = prices
def add_weather_event(self, date: datetime, event_type: str,
region: str, severity: str) -> None:
"""Record weather event affecting crop."""
impact_map = {
('drought', 'severe'): 'very_bullish',
('drought', 'moderate'): 'bullish',
('frost', 'severe'): 'very_bullish',
('frost', 'moderate'): 'bullish',
('flooding', 'severe'): 'bullish',
('ideal', 'any'): 'bearish'
}
impact = impact_map.get((event_type, severity), 'neutral')
self.weather_events.append({
'date': date,
'event': event_type,
'region': region,
'severity': severity,
'impact': impact
})
def get_seasonal_bias(self, month: int = None) -> Dict:
"""Get seasonal bias for current or specified month."""
if month is None:
month = datetime.now().month
pattern = self.SEASONAL_PATTERNS.get(self.commodity, {})
bias = pattern.get(month, 'unknown')
return {
'commodity': self.commodity,
'month': month,
'seasonal_bias': bias,
'explanation': self._get_seasonal_explanation(month)
}
def _get_seasonal_explanation(self, month: int) -> str:
"""Get explanation for seasonal pattern."""
explanations = {
'corn': {
(3, 5): 'Pre-planting uncertainty typically bullish',
(6, 8): 'Weather-driven volatility during growing season',
(9, 11): 'Harvest pressure typically bearish'
},
'soybeans': {
(2, 5): 'South American harvest, US planting',
(6, 8): 'Critical growing period',
(9, 11): 'US harvest pressure'
}
}
commodity_exp = explanations.get(self.commodity, {})
for (start, end), exp in commodity_exp.items():
if start <= month <= end:
return exp
return 'Standard seasonal period'
def calculate_historical_seasonality(self) -> pd.DataFrame:
"""Calculate historical seasonal performance."""
if self.prices.empty:
return pd.DataFrame()
returns = self.prices.pct_change() * 100
monthly = returns.groupby(returns.index.month).agg(['mean', 'std', 'count'])
monthly.columns = ['avg_return', 'volatility', 'observations']
monthly['win_rate'] = returns.groupby(returns.index.month).apply(
lambda x: (x > 0).sum() / len(x) * 100 if len(x) > 0 else 0
)
return monthly
def weather_signal(self) -> Dict:
"""Generate signal from recent weather events."""
if not self.weather_events:
return {'signal': 'neutral', 'reason': 'No weather events'}
# Look at events in last 30 days
cutoff = datetime.now() - timedelta(days=30)
recent = [e for e in self.weather_events if e['date'] > cutoff]
if not recent:
return {'signal': 'neutral', 'reason': 'No recent weather events'}
# Score events
impact_scores = {'very_bullish': 2, 'bullish': 1, 'neutral': 0, 'bearish': -1}
total_score = sum(impact_scores.get(e['impact'], 0) for e in recent)
if total_score >= 2:
signal = 'buy'
elif total_score <= -1:
signal = 'sell'
else:
signal = 'neutral'
return {
'signal': signal,
'events': len(recent),
'score': total_score,
'recent_events': recent[-3:]
}
# Demo
corn_analyzer = AgriculturalAnalyzer('corn')
corn_analyzer.set_prices(pd.Series(450 + np.cumsum(np.random.normal(0, 5, 60)), index=dates))
# Add weather events
corn_analyzer.add_weather_event(datetime.now() - timedelta(days=5), 'drought', 'US Midwest', 'moderate')
print(f"Corn Seasonal Bias (April): {corn_analyzer.get_seasonal_bias(4)}")
print(f"\nWeather Signal: {corn_analyzer.weather_signal()}")
7.4 Commodity Currencies
Commodity currencies (AUD, CAD, NOK) are strongly correlated with their primary export commodities.
Key Commodity Currency Relationships
| Currency | Primary Commodity | Correlation | Notes |
|---|---|---|---|
| AUD | Gold, Iron Ore | Positive | China demand key |
| CAD | Crude Oil | Positive | Oil sands production |
| NOK | Brent Crude | Positive | North Sea production |
| NZD | Dairy | Positive | Fonterra prices |
| ZAR | Gold, Platinum | Positive | Mining sector |
class CommodityCurrencyAnalyzer:
"""Analyze relationships between commodities and currencies."""
RELATIONSHIPS = {
'AUD': {'commodities': ['gold', 'iron_ore'], 'expected_corr': 0.6},
'CAD': {'commodities': ['oil'], 'expected_corr': 0.5},
'NOK': {'commodities': ['brent'], 'expected_corr': 0.6},
'NZD': {'commodities': ['dairy'], 'expected_corr': 0.4},
'ZAR': {'commodities': ['gold', 'platinum'], 'expected_corr': 0.5}
}
def __init__(self):
self.currency_data: Dict[str, pd.Series] = {}
self.commodity_data: Dict[str, pd.Series] = {}
def add_currency(self, currency: str, prices: pd.Series) -> None:
"""Add currency pair data."""
self.currency_data[currency] = prices
def add_commodity(self, commodity: str, prices: pd.Series) -> None:
"""Add commodity price data."""
self.commodity_data[commodity] = prices
def calculate_correlation(self, currency: str, commodity: str,
window: int = 20) -> float:
"""Calculate rolling correlation."""
if currency not in self.currency_data or commodity not in self.commodity_data:
return 0.0
curr_ret = self.currency_data[currency].pct_change()
comm_ret = self.commodity_data[commodity].pct_change()
aligned = pd.concat([curr_ret, comm_ret], axis=1).dropna()
if len(aligned) < window:
return 0.0
return aligned.iloc[-window:, 0].corr(aligned.iloc[-window:, 1])
def check_divergence(self, currency: str) -> Dict:
"""Check for divergence between currency and its commodities."""
if currency not in self.RELATIONSHIPS:
return {'currency': currency, 'divergence': False}
rel = self.RELATIONSHIPS[currency]
expected = rel['expected_corr']
correlations = []
for comm in rel['commodities']:
if comm in self.commodity_data:
corr = self.calculate_correlation(currency, comm)
correlations.append({'commodity': comm, 'correlation': corr})
if not correlations:
return {'currency': currency, 'divergence': False, 'reason': 'No commodity data'}
avg_corr = np.mean([c['correlation'] for c in correlations])
divergence = avg_corr < expected * 0.5 or avg_corr < 0
return {
'currency': currency,
'expected_correlation': expected,
'actual_correlation': avg_corr,
'divergence': divergence,
'signal': 'divergence_trade' if divergence else 'follow_commodity',
'details': correlations
}
def commodity_currency_signal(self, currency: str) -> Dict:
"""Generate trading signal based on commodity-currency relationship."""
if currency not in self.RELATIONSHIPS:
return {'signal': 'no_relationship'}
rel = self.RELATIONSHIPS[currency]
signals = []
for comm in rel['commodities']:
if comm not in self.commodity_data:
continue
comm_prices = self.commodity_data[comm]
comm_ret_20d = (comm_prices.iloc[-1] / comm_prices.iloc[-20] - 1) * 100 if len(comm_prices) >= 20 else 0
if comm_ret_20d > 5:
signals.append({'commodity': comm, 'signal': 'bullish', 'return': comm_ret_20d})
elif comm_ret_20d < -5:
signals.append({'commodity': comm, 'signal': 'bearish', 'return': comm_ret_20d})
else:
signals.append({'commodity': comm, 'signal': 'neutral', 'return': comm_ret_20d})
if not signals:
return {'signal': 'no_data'}
bullish = sum(1 for s in signals if s['signal'] == 'bullish')
bearish = sum(1 for s in signals if s['signal'] == 'bearish')
if bullish > bearish:
overall = 'buy_currency'
elif bearish > bullish:
overall = 'sell_currency'
else:
overall = 'neutral'
return {
'currency': currency,
'signal': overall,
'commodity_signals': signals
}
# Demo
cc_analyzer = CommodityCurrencyAnalyzer()
# Add data
cc_analyzer.add_currency('AUD', pd.Series(0.65 + np.cumsum(np.random.normal(0, 0.002, 60)), index=dates))
cc_analyzer.add_currency('CAD', pd.Series(1.35 + np.cumsum(np.random.normal(0, 0.002, 60)), index=dates))
cc_analyzer.add_commodity('gold', pd.Series(2000 + np.cumsum(np.random.normal(2, 15, 60)), index=dates))
cc_analyzer.add_commodity('oil', pd.Series(75 + np.cumsum(np.random.normal(0.5, 1, 60)), index=dates))
print("AUD-Gold Divergence Check:")
print(cc_analyzer.check_divergence('AUD'))
print("\nCAD Trading Signal:")
print(cc_analyzer.commodity_currency_signal('CAD'))
Exercises
Exercise 1: Gold Safe Haven Analyzer (Guided)
Complete the SafeHavenAnalyzer class that tracks gold's safe-haven behavior.
class SafeHavenAnalyzer:
"""Analyze gold's safe-haven characteristics."""
def __init__(self):
self.gold_prices: pd.Series = pd.Series(dtype=float)
self.spx_prices: pd.Series = pd.Series(dtype=float)
self.vix_prices: pd.Series = pd.Series(dtype=float)
def set_data(self, gold: pd.Series, spx: pd.Series, vix: pd.Series) -> None:
"""Set market data."""
self.gold_prices = gold
self.spx_prices = spx
self.vix_prices = vix
def calculate_crisis_correlation(self, vix_threshold: float = 25) -> float:
"""Calculate gold-SPX correlation during high VIX periods."""
gold_ret = self.gold_prices.pct_change()
spx_ret = self.spx_prices.pct_change()
# Filter for crisis periods
crisis_mask = self.vix_prices ______ vix_threshold
crisis_gold = gold_ret[crisis_mask]
crisis_spx = spx_ret[crisis_mask]
if len(crisis_gold) < 5:
return 0.0
return crisis_gold.______(crisis_spx)
def safe_haven_score(self) -> Dict:
"""Calculate safe-haven effectiveness score."""
crisis_corr = self.calculate_crisis_correlation()
# Good safe haven has negative correlation during crisis
if crisis_corr < -0.3:
score = 100
rating = 'excellent'
elif crisis_corr < 0:
score = 70
rating = 'good'
elif crisis_corr < 0.3:
score = ______
rating = 'moderate'
else:
score = 20
rating = 'poor'
return {
'crisis_correlation': crisis_corr,
'safe_haven_score': score,
'rating': rating
}
# Test
sha = SafeHavenAnalyzer()
sha.set_data(
gold=pd.Series(2000 + np.cumsum(np.random.normal(1, 10, 100)), index=pd.date_range('2024-01-01', periods=100)),
spx=pd.Series(5000 + np.cumsum(np.random.normal(0, 20, 100)), index=pd.date_range('2024-01-01', periods=100)),
vix=pd.Series(np.clip(18 + np.cumsum(np.random.normal(0, 2, 100)), 10, 50), index=pd.date_range('2024-01-01', periods=100))
)
print(f"Safe Haven Analysis: {sha.safe_haven_score()}")
Solution 1
class SafeHavenAnalyzer:
def __init__(self):
self.gold_prices: pd.Series = pd.Series(dtype=float)
self.spx_prices: pd.Series = pd.Series(dtype=float)
self.vix_prices: pd.Series = pd.Series(dtype=float)
def set_data(self, gold: pd.Series, spx: pd.Series, vix: pd.Series) -> None:
self.gold_prices = gold
self.spx_prices = spx
self.vix_prices = vix
def calculate_crisis_correlation(self, vix_threshold: float = 25) -> float:
gold_ret = self.gold_prices.pct_change()
spx_ret = self.spx_prices.pct_change()
crisis_mask = self.vix_prices > vix_threshold
crisis_gold = gold_ret[crisis_mask]
crisis_spx = spx_ret[crisis_mask]
if len(crisis_gold) < 5:
return 0.0
return crisis_gold.corr(crisis_spx)
def safe_haven_score(self) -> Dict:
crisis_corr = self.calculate_crisis_correlation()
if crisis_corr < -0.3:
score = 100
rating = 'excellent'
elif crisis_corr < 0:
score = 70
rating = 'good'
elif crisis_corr < 0.3:
score = 40
rating = 'moderate'
else:
score = 20
rating = 'poor'
return {
'crisis_correlation': crisis_corr,
'safe_haven_score': score,
'rating': rating
}
Exercise 2: Oil Inventory Tracker (Guided)
Complete the InventoryTracker class that analyzes EIA oil inventory data.
class InventoryTracker:
"""Track and analyze oil inventory data."""
def __init__(self):
self.reports: List[Dict] = []
def add_report(self, date: datetime, crude: float, gasoline: float,
distillate: float) -> None:
"""Add weekly inventory report (values in millions of barrels)."""
self.reports.append({
'date': date,
'crude': crude,
'gasoline': gasoline,
'distillate': distillate,
'total': crude + gasoline + distillate
})
def get_weekly_change(self) -> Dict:
"""Calculate week-over-week inventory changes."""
if len(self.reports) < 2:
return {}
latest = self.reports[-1]
previous = self.reports[______]
return {
'crude_change': latest['crude'] - previous['crude'],
'gasoline_change': latest['gasoline'] - previous['gasoline'],
'distillate_change': latest['distillate'] - previous['distillate'],
'total_change': latest['total'] - previous['total']
}
def get_trend(self, weeks: int = 4) -> str:
"""Determine inventory trend over N weeks."""
if len(self.reports) < weeks + 1:
return 'insufficient_data'
builds = 0
for i in range(1, weeks + 1):
if self.reports[-i]['total'] > self.reports[-i-1]['______']:
builds += 1
if builds >= weeks - 1:
return 'building'
elif builds <= 1:
return 'drawing'
return 'mixed'
def generate_signal(self) -> Dict:
"""Generate trading signal from inventory data."""
change = self.get_weekly_change()
trend = self.get_trend()
if not change:
return {'signal': 'no_data'}
total_change = change['total_change']
if total_change < -5 and trend == 'drawing':
signal = 'strong_buy'
elif total_change < 0:
signal = 'buy'
elif total_change > 5 and trend == 'building':
signal = 'strong_sell'
elif total_change > 0:
signal = 'sell'
else:
signal = 'neutral'
return {'signal': signal, 'weekly_change': total_change, 'trend': trend}
# Test
tracker = InventoryTracker()
tracker.add_report(datetime(2024, 4, 3), 450, 230, 120)
tracker.add_report(datetime(2024, 4, 10), 448, 228, 118)
tracker.add_report(datetime(2024, 4, 17), 445, 225, 117)
tracker.add_report(datetime(2024, 4, 24), 442, 223, 115)
tracker.add_report(datetime(2024, 5, 1), 438, 220, 113)
print(f"Weekly Change: {tracker.get_weekly_change()}")
print(f"Trend: {tracker.get_trend()}")
print(f"Signal: {tracker.generate_signal()}")
Solution 2
class InventoryTracker:
def __init__(self):
self.reports: List[Dict] = []
def add_report(self, date: datetime, crude: float, gasoline: float,
distillate: float) -> None:
self.reports.append({
'date': date,
'crude': crude,
'gasoline': gasoline,
'distillate': distillate,
'total': crude + gasoline + distillate
})
def get_weekly_change(self) -> Dict:
if len(self.reports) < 2:
return {}
latest = self.reports[-1]
previous = self.reports[-2]
return {
'crude_change': latest['crude'] - previous['crude'],
'gasoline_change': latest['gasoline'] - previous['gasoline'],
'distillate_change': latest['distillate'] - previous['distillate'],
'total_change': latest['total'] - previous['total']
}
def get_trend(self, weeks: int = 4) -> str:
if len(self.reports) < weeks + 1:
return 'insufficient_data'
builds = 0
for i in range(1, weeks + 1):
if self.reports[-i]['total'] > self.reports[-i-1]['total']:
builds += 1
if builds >= weeks - 1:
return 'building'
elif builds <= 1:
return 'drawing'
return 'mixed'
Exercise 3: Seasonal Pattern Detector (Guided)
Complete the SeasonalDetector class that identifies seasonal trading opportunities.
class SeasonalDetector:
"""Detect and trade seasonal patterns in commodities."""
def __init__(self):
self.prices: pd.Series = pd.Series(dtype=float)
def set_prices(self, prices: pd.Series) -> None:
"""Set historical price data."""
self.prices = prices
def calculate_monthly_stats(self) -> pd.DataFrame:
"""Calculate statistics for each month."""
returns = self.prices.pct_change() * 100
monthly = returns.groupby(returns.index.______).agg([
('avg_return', 'mean'),
('volatility', 'std'),
('win_rate', lambda x: (x > 0).sum() / len(x) * 100)
])
return monthly
def get_best_months(self, top_n: int = 3) -> List[int]:
"""Get months with best historical performance."""
stats = self.calculate_monthly_stats()
return stats.nlargest(top_n, 'avg_return').______.tolist()
def get_worst_months(self, bottom_n: int = 3) -> List[int]:
"""Get months with worst historical performance."""
stats = self.calculate_monthly_stats()
return stats.nsmallest(bottom_n, 'avg_return').index.tolist()
def current_month_outlook(self) -> Dict:
"""Get outlook for current month based on seasonality."""
current_month = datetime.now().______
stats = self.calculate_monthly_stats()
if current_month not in stats.index:
return {'outlook': 'no_data'}
month_stats = stats.loc[current_month]
if month_stats['avg_return'] > 1 and month_stats['win_rate'] > 60:
outlook = 'bullish'
elif month_stats['avg_return'] < -1 and month_stats['win_rate'] < 40:
outlook = 'bearish'
else:
outlook = 'neutral'
return {
'month': current_month,
'outlook': outlook,
'avg_return': month_stats['avg_return'],
'win_rate': month_stats['win_rate']
}
# Test
detector = SeasonalDetector()
# Create 2 years of simulated data
dates_2y = pd.date_range('2022-01-01', periods=500, freq='D')
detector.set_prices(pd.Series(100 + np.cumsum(np.random.normal(0.02, 1, 500)), index=dates_2y))
print(f"Best Months: {detector.get_best_months()}")
print(f"Worst Months: {detector.get_worst_months()}")
print(f"Current Outlook: {detector.current_month_outlook()}")
Solution 3
class SeasonalDetector:
def __init__(self):
self.prices: pd.Series = pd.Series(dtype=float)
def set_prices(self, prices: pd.Series) -> None:
self.prices = prices
def calculate_monthly_stats(self) -> pd.DataFrame:
returns = self.prices.pct_change() * 100
monthly = returns.groupby(returns.index.month).agg([
('avg_return', 'mean'),
('volatility', 'std'),
('win_rate', lambda x: (x > 0).sum() / len(x) * 100)
])
return monthly
def get_best_months(self, top_n: int = 3) -> List[int]:
stats = self.calculate_monthly_stats()
return stats.nlargest(top_n, 'avg_return').index.tolist()
def get_worst_months(self, bottom_n: int = 3) -> List[int]:
stats = self.calculate_monthly_stats()
return stats.nsmallest(bottom_n, 'avg_return').index.tolist()
def current_month_outlook(self) -> Dict:
current_month = datetime.now().month
stats = self.calculate_monthly_stats()
if current_month not in stats.index:
return {'outlook': 'no_data'}
month_stats = stats.loc[current_month]
if month_stats['avg_return'] > 1 and month_stats['win_rate'] > 60:
outlook = 'bullish'
elif month_stats['avg_return'] < -1 and month_stats['win_rate'] < 40:
outlook = 'bearish'
else:
outlook = 'neutral'
return {
'month': current_month,
'outlook': outlook,
'avg_return': month_stats['avg_return'],
'win_rate': month_stats['win_rate']
}
Exercise 4: Complete Gold Trading System (Open-ended)
Build a comprehensive gold trading system that: - Tracks USD correlation - Monitors real yields impact - Detects safe-haven flows - Generates actionable signals
# Exercise 4: Complete Gold Trading System (Open-ended)
#
# Requirements:
# 1. Create class GoldTradingSystem
# 2. Track: gold prices, DXY, real yields, VIX, SPX
# 3. Calculate gold-USD correlation (should be negative)
# 4. Monitor real yields impact (negative correlation with gold)
# 5. Detect safe-haven demand (VIX spikes)
# 6. Combine factors into weighted signal
#
# Your implementation:
Solution 4
class GoldTradingSystem:
def __init__(self):
self.gold: pd.Series = pd.Series(dtype=float)
self.dxy: pd.Series = pd.Series(dtype=float)
self.real_yields: pd.Series = pd.Series(dtype=float)
self.vix: pd.Series = pd.Series(dtype=float)
self.spx: pd.Series = pd.Series(dtype=float)
def set_data(self, gold, dxy, real_yields, vix, spx):
self.gold = gold
self.dxy = dxy
self.real_yields = real_yields
self.vix = vix
self.spx = spx
def usd_signal(self) -> Dict:
corr = self.gold.pct_change().rolling(20).corr(self.dxy.pct_change()).iloc[-1]
dxy_trend = self.dxy.iloc[-1] / self.dxy.iloc[-20] - 1
if dxy_trend < -0.02: # USD weakening
return {'signal': 'buy', 'reason': 'USD weakness'}
elif dxy_trend > 0.02: # USD strengthening
return {'signal': 'sell', 'reason': 'USD strength'}
return {'signal': 'neutral', 'reason': 'USD stable'}
def yield_signal(self) -> Dict:
current = self.real_yields.iloc[-1]
ma = self.real_yields.rolling(20).mean().iloc[-1]
if current < 0:
return {'signal': 'buy', 'reason': 'Negative real yields'}
elif current > ma + 0.5:
return {'signal': 'sell', 'reason': 'Rising real yields'}
return {'signal': 'neutral', 'reason': 'Yields stable'}
def safe_haven_signal(self) -> Dict:
vix_current = self.vix.iloc[-1]
vix_change = vix_current - self.vix.iloc[-5]
if vix_current > 25 and vix_change > 5:
return {'signal': 'strong_buy', 'reason': 'Fear spike'}
elif vix_current > 20:
return {'signal': 'buy', 'reason': 'Elevated fear'}
elif vix_current < 15:
return {'signal': 'sell', 'reason': 'Risk-on'}
return {'signal': 'neutral', 'reason': 'Normal vol'}
def combined_signal(self) -> Dict:
signals = [
('usd', self.usd_signal(), 0.3),
('yield', self.yield_signal(), 0.3),
('safe_haven', self.safe_haven_signal(), 0.4)
]
score_map = {'strong_buy': 2, 'buy': 1, 'neutral': 0, 'sell': -1, 'strong_sell': -2}
total = sum(score_map.get(s[1]['signal'], 0) * s[2] for s in signals)
if total > 0.5: return {'signal': 'buy', 'score': total}
elif total < -0.5: return {'signal': 'sell', 'score': total}
return {'signal': 'neutral', 'score': total}
Exercise 5: Oil Supply/Demand Analyzer (Open-ended)
Build an oil market analyzer that: - Tracks EIA inventory data - Monitors OPEC production changes - Calculates supply/demand balance - Generates trading signals
# Exercise 5: Oil Supply/Demand Analyzer (Open-ended)
#
# Requirements:
# 1. Create class OilSupplyDemandAnalyzer
# 2. Track: inventories, OPEC production, global demand estimates
# 3. Calculate implied supply/demand balance
# 4. Detect inventory trends (builds vs draws)
# 5. Generate trading signals from S/D balance
#
# Your implementation:
Solution 5
class OilSupplyDemandAnalyzer:
def __init__(self):
self.inventories: List[Dict] = []
self.opec_production: List[Dict] = []
self.demand_estimates: List[Dict] = []
def add_inventory(self, date, crude, gasoline, distillate):
self.inventories.append({
'date': date,
'crude': crude,
'gasoline': gasoline,
'distillate': distillate,
'total': crude + gasoline + distillate
})
def add_opec_data(self, date, production, quota):
self.opec_production.append({
'date': date,
'production': production,
'quota': quota,
'compliance': (quota - production) / quota * 100
})
def add_demand_estimate(self, date, demand, yoy_change):
self.demand_estimates.append({
'date': date,
'demand': demand,
'yoy_change': yoy_change
})
def inventory_trend(self, weeks=4):
if len(self.inventories) < weeks + 1:
return 'insufficient_data'
changes = [self.inventories[-i]['total'] - self.inventories[-i-1]['total']
for i in range(1, weeks + 1)]
draws = sum(1 for c in changes if c < 0)
return 'drawing' if draws >= weeks - 1 else ('building' if draws <= 1 else 'mixed')
def supply_demand_balance(self):
if not self.opec_production or not self.demand_estimates:
return {'balance': 'no_data'}
supply = self.opec_production[-1]['production']
demand = self.demand_estimates[-1]['demand']
balance = supply - demand
return {
'supply': supply,
'demand': demand,
'balance': balance,
'status': 'surplus' if balance > 0.5 else ('deficit' if balance < -0.5 else 'balanced')
}
def generate_signal(self):
inv_trend = self.inventory_trend()
sd_balance = self.supply_demand_balance()
score = 0
if inv_trend == 'drawing': score += 1
elif inv_trend == 'building': score -= 1
if sd_balance.get('status') == 'deficit': score += 1
elif sd_balance.get('status') == 'surplus': score -= 1
signal = 'buy' if score > 0 else ('sell' if score < 0 else 'neutral')
return {'signal': signal, 'score': score, 'inventory_trend': inv_trend, 'sd_status': sd_balance.get('status')}
Exercise 6: Commodity Currency Strategy (Open-ended)
Build a trading strategy that exploits commodity-currency relationships: - Track multiple commodity-currency pairs - Detect correlation breakdowns - Generate pair trading signals - Manage portfolio of commodity FX trades
# Exercise 6: Commodity Currency Strategy (Open-ended)
#
# Requirements:
# 1. Create class CommodityCurrencyStrategy
# 2. Define relationships: AUD-Gold, CAD-Oil, NOK-Brent
# 3. Track correlations over rolling windows
# 4. Detect divergences (correlation breakdown)
# 5. Generate signals: follow commodity or trade divergence
# 6. Rank opportunities by strength
#
# Your implementation:
Solution 6
class CommodityCurrencyStrategy:
PAIRS = {
'AUDUSD': {'commodity': 'gold', 'expected_corr': 0.6},
'USDCAD': {'commodity': 'oil', 'expected_corr': -0.5}, # Inverted
'USDNOK': {'commodity': 'brent', 'expected_corr': -0.6}
}
def __init__(self):
self.currencies: Dict[str, pd.Series] = {}
self.commodities: Dict[str, pd.Series] = {}
def add_data(self, symbol: str, prices: pd.Series, is_commodity: bool = False):
if is_commodity:
self.commodities[symbol] = prices
else:
self.currencies[symbol] = prices
def rolling_correlation(self, pair: str, window: int = 20) -> float:
if pair not in self.PAIRS:
return 0.0
commodity = self.PAIRS[pair]['commodity']
if pair not in self.currencies or commodity not in self.commodities:
return 0.0
curr_ret = self.currencies[pair].pct_change()
comm_ret = self.commodities[commodity].pct_change()
aligned = pd.concat([curr_ret, comm_ret], axis=1).dropna()
return aligned.iloc[-window:, 0].corr(aligned.iloc[-window:, 1])
def detect_divergence(self, pair: str, threshold: float = 0.3) -> Dict:
expected = self.PAIRS[pair]['expected_corr']
actual = self.rolling_correlation(pair)
divergence = abs(actual - expected) > threshold
return {
'pair': pair,
'expected': expected,
'actual': actual,
'divergence': divergence
}
def generate_signals(self) -> List[Dict]:
signals = []
for pair in self.PAIRS:
div = self.detect_divergence(pair)
commodity = self.PAIRS[pair]['commodity']
if commodity not in self.commodities:
continue
comm_ret = (self.commodities[commodity].iloc[-1] /
self.commodities[commodity].iloc[-20] - 1) * 100
if div['divergence']:
signal = 'divergence_trade'
elif comm_ret > 3:
signal = 'buy' if self.PAIRS[pair]['expected_corr'] > 0 else 'sell'
elif comm_ret < -3:
signal = 'sell' if self.PAIRS[pair]['expected_corr'] > 0 else 'buy'
else:
signal = 'neutral'
signals.append({
'pair': pair,
'signal': signal,
'commodity_return': comm_ret,
'correlation': div['actual'],
'strength': abs(comm_ret) / 10
})
return sorted(signals, key=lambda x: x['strength'], reverse=True)
Module Project: Commodity Trading System
Build a production-ready system that integrates all commodity trading concepts.
class CommodityTradingSystem:
"""
Production-ready commodity trading system.
Integrates: Gold, Oil, Agricultural, Commodity Currencies.
Provides: Market analysis, Trading signals, Risk assessment.
"""
def __init__(self):
self.gold_analyzer = GoldAnalyzer()
self.oil_analyzer = OilAnalyzer()
self.commodity_currencies = CommodityCurrencyAnalyzer()
self.market_data: Dict[str, pd.Series] = {}
def load_market_data(self, symbol: str, prices: pd.Series) -> None:
"""Load price data for any market."""
self.market_data[symbol] = prices
# Route to appropriate analyzer
if symbol == 'GOLD' or symbol == 'XAUUSD':
self.gold_analyzer.set_data(gold=prices)
elif symbol in ['OIL', 'WTI', 'CL']:
self.oil_analyzer.set_prices(prices)
elif symbol in ['AUD', 'CAD', 'NOK', 'NZD']:
self.commodity_currencies.add_currency(symbol, prices)
elif symbol.lower() in ['gold', 'oil', 'brent', 'iron_ore', 'dairy']:
self.commodity_currencies.add_commodity(symbol.lower(), prices)
def add_oil_inventory(self, date: datetime, actual: float,
forecast: float, previous: float) -> None:
"""Add oil inventory report."""
self.oil_analyzer.add_inventory_report(date, actual, forecast, previous)
def get_gold_analysis(self) -> Dict:
"""Get comprehensive gold analysis."""
return {
'safe_haven': self.gold_analyzer.safe_haven_signal(),
'real_yield': self.gold_analyzer.real_yield_signal(),
'combined': self.gold_analyzer.combined_signal()
}
def get_oil_analysis(self) -> Dict:
"""Get comprehensive oil analysis."""
return {
'inventory_trend': self.oil_analyzer.inventory_trend(),
'inventory_signal': self.oil_analyzer.inventory_signal()
}
def get_commodity_fx_signals(self) -> List[Dict]:
"""Get all commodity currency signals."""
signals = []
for currency in ['AUD', 'CAD', 'NOK', 'NZD']:
if currency in self.commodity_currencies.currency_data:
signal = self.commodity_currencies.commodity_currency_signal(currency)
divergence = self.commodity_currencies.check_divergence(currency)
signals.append({
'currency': currency,
'signal': signal,
'divergence': divergence
})
return signals
def get_all_opportunities(self) -> List[Dict]:
"""Rank all trading opportunities."""
opportunities = []
# Gold opportunity
gold_signal = self.gold_analyzer.combined_signal()
if gold_signal['overall'] in ['buy', 'strong_buy', 'sell', 'strong_sell']:
opportunities.append({
'market': 'GOLD',
'direction': 'long' if 'buy' in gold_signal['overall'] else 'short',
'strength': abs(gold_signal['score']) / 4,
'reason': 'Combined fundamental signals'
})
# Oil opportunity
oil_signal = self.oil_analyzer.inventory_signal()
if oil_signal.get('signal') in ['buy', 'strong_buy', 'sell', 'strong_sell']:
opportunities.append({
'market': 'OIL',
'direction': 'long' if 'buy' in oil_signal['signal'] else 'short',
'strength': 0.7 if 'strong' in oil_signal['signal'] else 0.5,
'reason': oil_signal.get('reason', '')
})
# Commodity currencies
for fx_signal in self.get_commodity_fx_signals():
sig = fx_signal['signal'].get('signal', 'neutral')
if sig in ['buy_currency', 'sell_currency']:
opportunities.append({
'market': f"{fx_signal['currency']}USD",
'direction': 'long' if sig == 'buy_currency' else 'short',
'strength': 0.5,
'reason': 'Commodity correlation'
})
return sorted(opportunities, key=lambda x: x['strength'], reverse=True)
def print_dashboard(self) -> None:
"""Print comprehensive dashboard."""
print("\n" + "=" * 60)
print(" COMMODITY TRADING DASHBOARD")
print(f" {datetime.now().strftime('%Y-%m-%d %H:%M')}")
print("=" * 60)
# Gold Analysis
print("\n" + "-" * 40)
print("GOLD ANALYSIS")
print("-" * 40)
gold = self.get_gold_analysis()
print(f"Safe Haven: {gold['safe_haven'].get('signal', 'N/A')}")
print(f"Real Yield: {gold['real_yield'].get('signal', 'N/A')}")
print(f"Combined: {gold['combined'].get('overall', 'N/A')}")
# Oil Analysis
print("\n" + "-" * 40)
print("OIL ANALYSIS")
print("-" * 40)
oil = self.get_oil_analysis()
print(f"Inventory Trend: {oil['inventory_trend'].get('trend', 'N/A')}")
print(f"Signal: {oil['inventory_signal'].get('signal', 'N/A')}")
# Commodity Currencies
print("\n" + "-" * 40)
print("COMMODITY CURRENCIES")
print("-" * 40)
for fx in self.get_commodity_fx_signals():
print(f"{fx['currency']}: {fx['signal'].get('signal', 'N/A')}")
# Top Opportunities
print("\n" + "-" * 40)
print("TOP OPPORTUNITIES")
print("-" * 40)
for i, opp in enumerate(self.get_all_opportunities()[:5], 1):
print(f"{i}. {opp['market']} - {opp['direction'].upper()}")
print(f" Strength: {opp['strength']:.0%} | {opp['reason']}")
print("\n" + "=" * 60)
# Demo the system
np.random.seed(42)
dates = pd.date_range('2024-01-01', periods=60, freq='D')
system = CommodityTradingSystem()
# Load market data
system.load_market_data('GOLD', pd.Series(2000 + np.cumsum(np.random.normal(1, 15, 60)), index=dates))
system.load_market_data('OIL', pd.Series(75 + np.cumsum(np.random.normal(0.2, 1, 60)), index=dates))
system.load_market_data('AUD', pd.Series(0.65 + np.cumsum(np.random.normal(0.0005, 0.003, 60)), index=dates))
system.load_market_data('CAD', pd.Series(1.35 + np.cumsum(np.random.normal(-0.0003, 0.002, 60)), index=dates))
system.load_market_data('gold', pd.Series(2000 + np.cumsum(np.random.normal(1, 15, 60)), index=dates))
system.load_market_data('oil', pd.Series(75 + np.cumsum(np.random.normal(0.2, 1, 60)), index=dates))
# Set additional gold data
system.gold_analyzer.set_data(
gold=system.market_data['GOLD'],
dxy=pd.Series(104 + np.cumsum(np.random.normal(-0.05, 0.3, 60)), index=dates),
vix=pd.Series(np.clip(18 + np.cumsum(np.random.normal(0.1, 1, 60)), 10, 40), index=dates),
real_yields=pd.Series(0.5 + np.cumsum(np.random.normal(-0.01, 0.05, 60)), index=dates)
)
# Add oil inventory data
system.add_oil_inventory(datetime(2024, 4, 3), 451.2, 453.0, 452.5)
system.add_oil_inventory(datetime(2024, 4, 10), 449.8, 451.0, 451.2)
system.add_oil_inventory(datetime(2024, 4, 17), 448.5, 450.5, 449.8)
system.add_oil_inventory(datetime(2024, 4, 24), 446.2, 449.0, 448.5)
# Print dashboard
system.print_dashboard()
Key Takeaways
- Gold Trading: Driven by USD, real yields, and safe-haven demand; negative correlation with risk assets
- Oil Analysis: Track EIA inventories (Wed), OPEC decisions, and supply/demand balance
- Inventory Impact: Draws = bullish, Builds = bearish; surprises move markets most
- Agricultural Seasonality: Strong patterns around planting/harvest; weather is key risk
- Commodity Currencies: AUD-Gold, CAD-Oil correlations create trading opportunities
- Divergence Trading: When correlations break down, mean reversion opportunities emerge
- Multi-Asset Integration: Combine commodity and FX analysis for better signals
- Risk Management: Commodities are volatile; size positions appropriately
Next: Module 8 - Trading Strategies where we'll build trend following, mean reversion, carry, and news trading systems.
Module 8: Trading Strategies
Part 2: Analysis & Strategies
| Duration | Exercises |
|---|---|
| ~2.5 hours | 6 |
Learning Objectives
- Build forex trend following systems with MA and breakout strategies
- Implement range trading and mean reversion strategies
- Understand carry trade mechanics and interest rate differentials
- Develop news trading strategies for high-impact events
Prerequisites
- Modules 1-7 (Forex/Futures fundamentals, analysis, commodities)
- Understanding of technical and fundamental analysis
- Python pandas and backtesting concepts
import pandas as pd
import numpy as np
from datetime import datetime, timedelta
from dataclasses import dataclass, field
from typing import List, Dict, Tuple, Optional
from enum import Enum
import warnings
warnings.filterwarnings('ignore')
8.1 Forex Trend Following
Trend following strategies capture extended directional moves in currency pairs using moving averages and breakout systems.
Trend Following Approaches
| Strategy | Entry Signal | Exit Signal | Best Market |
|---|---|---|---|
| MA Crossover | Fast MA crosses slow MA | Opposite crossover | Trending |
| Channel Breakout | Price breaks N-period high/low | Opposite breakout | Volatile |
| ADX Filter | Trade with trend when ADX > 25 | ADX falls below 20 | Strong trends |
| Donchian | Price breaks 20-day high/low | 10-day opposite | Any |
class MovingAverageCrossover:
"""Classic moving average crossover trend following strategy."""
def __init__(self, fast_period: int = 20, slow_period: int = 50):
self.fast_period = fast_period
self.slow_period = slow_period
self.prices: pd.Series = pd.Series(dtype=float)
def set_data(self, prices: pd.Series) -> None:
"""Set price data."""
self.prices = prices
def calculate_signals(self) -> pd.DataFrame:
"""Calculate MA crossover signals."""
df = pd.DataFrame({'price': self.prices})
df['fast_ma'] = df['price'].rolling(self.fast_period).mean()
df['slow_ma'] = df['price'].rolling(self.slow_period).mean()
# Signal: 1 = long, -1 = short, 0 = no position
df['signal'] = 0
df.loc[df['fast_ma'] > df['slow_ma'], 'signal'] = 1
df.loc[df['fast_ma'] < df['slow_ma'], 'signal'] = -1
# Trade signals (changes in position)
df['trade'] = df['signal'].diff()
return df
def get_current_signal(self) -> Dict:
"""Get current trading signal."""
df = self.calculate_signals()
if len(df) < self.slow_period:
return {'signal': 'no_data'}
latest = df.iloc[-1]
return {
'signal': 'long' if latest['signal'] == 1 else ('short' if latest['signal'] == -1 else 'flat'),
'fast_ma': latest['fast_ma'],
'slow_ma': latest['slow_ma'],
'price': latest['price'],
'trade': 'buy' if latest['trade'] == 2 else ('sell' if latest['trade'] == -2 else 'hold')
}
def backtest(self) -> Dict:
"""Simple backtest of the strategy."""
df = self.calculate_signals()
df['returns'] = df['price'].pct_change()
df['strategy_returns'] = df['signal'].shift(1) * df['returns']
total_return = (1 + df['strategy_returns'].dropna()).prod() - 1
buy_hold = (1 + df['returns'].dropna()).prod() - 1
trades = df['trade'].abs().sum() / 2
return {
'strategy_return': total_return * 100,
'buy_hold_return': buy_hold * 100,
'outperformance': (total_return - buy_hold) * 100,
'num_trades': int(trades),
'sharpe': df['strategy_returns'].mean() / df['strategy_returns'].std() * np.sqrt(252) if df['strategy_returns'].std() > 0 else 0
}
# Demo
np.random.seed(42)
dates = pd.date_range('2024-01-01', periods=120, freq='D')
# Create trending data
trend = np.cumsum(np.random.normal(0.001, 0.01, 120))
prices = pd.Series(1.1000 + trend, index=dates)
ma_strategy = MovingAverageCrossover(fast_period=10, slow_period=30)
ma_strategy.set_data(prices)
print("MA Crossover Strategy:")
print(f"Current Signal: {ma_strategy.get_current_signal()}")
print(f"\nBacktest Results: {ma_strategy.backtest()}")
class BreakoutStrategy:
"""Donchian channel breakout strategy."""
def __init__(self, entry_period: int = 20, exit_period: int = 10):
self.entry_period = entry_period
self.exit_period = exit_period
self.prices: pd.DataFrame = pd.DataFrame()
def set_data(self, high: pd.Series, low: pd.Series, close: pd.Series) -> None:
"""Set OHLC data."""
self.prices = pd.DataFrame({
'high': high,
'low': low,
'close': close
})
def calculate_channels(self) -> pd.DataFrame:
"""Calculate Donchian channels."""
df = self.prices.copy()
# Entry channels
df['entry_high'] = df['high'].rolling(self.entry_period).max()
df['entry_low'] = df['low'].rolling(self.entry_period).min()
# Exit channels
df['exit_high'] = df['high'].rolling(self.exit_period).max()
df['exit_low'] = df['low'].rolling(self.exit_period).min()
return df
def calculate_signals(self) -> pd.DataFrame:
"""Calculate breakout signals."""
df = self.calculate_channels()
df['signal'] = 0
position = 0
signals = []
for i in range(len(df)):
if i < self.entry_period:
signals.append(0)
continue
close = df['close'].iloc[i]
prev_entry_high = df['entry_high'].iloc[i-1]
prev_entry_low = df['entry_low'].iloc[i-1]
prev_exit_high = df['exit_high'].iloc[i-1]
prev_exit_low = df['exit_low'].iloc[i-1]
# Entry signals
if position == 0:
if close > prev_entry_high:
position = 1
elif close < prev_entry_low:
position = -1
# Exit signals
elif position == 1:
if close < prev_exit_low:
position = 0
elif position == -1:
if close > prev_exit_high:
position = 0
signals.append(position)
df['signal'] = signals
df['trade'] = df['signal'].diff()
return df
def get_current_signal(self) -> Dict:
"""Get current signal and levels."""
df = self.calculate_signals()
latest = df.iloc[-1]
return {
'position': 'long' if latest['signal'] == 1 else ('short' if latest['signal'] == -1 else 'flat'),
'entry_high': latest['entry_high'],
'entry_low': latest['entry_low'],
'close': latest['close'],
'trade': 'entry' if abs(latest['trade']) > 0 else 'hold'
}
# Demo
np.random.seed(42)
high = pd.Series(1.1000 + trend + np.random.uniform(0, 0.005, 120), index=dates)
low = pd.Series(1.1000 + trend - np.random.uniform(0, 0.005, 120), index=dates)
close = prices
breakout = BreakoutStrategy(entry_period=20, exit_period=10)
breakout.set_data(high, low, close)
print("Breakout Strategy:")
print(f"Current Signal: {breakout.get_current_signal()}")
class TrendFilter:
"""ADX-based trend filter to improve trend following signals."""
def __init__(self, adx_period: int = 14, adx_threshold: float = 25):
self.adx_period = adx_period
self.adx_threshold = adx_threshold
def calculate_adx(self, high: pd.Series, low: pd.Series,
close: pd.Series) -> pd.DataFrame:
"""Calculate ADX indicator."""
df = pd.DataFrame({'high': high, 'low': low, 'close': close})
# True Range
df['tr1'] = df['high'] - df['low']
df['tr2'] = abs(df['high'] - df['close'].shift(1))
df['tr3'] = abs(df['low'] - df['close'].shift(1))
df['tr'] = df[['tr1', 'tr2', 'tr3']].max(axis=1)
# Directional Movement
df['up_move'] = df['high'] - df['high'].shift(1)
df['down_move'] = df['low'].shift(1) - df['low']
df['+dm'] = np.where((df['up_move'] > df['down_move']) & (df['up_move'] > 0), df['up_move'], 0)
df['-dm'] = np.where((df['down_move'] > df['up_move']) & (df['down_move'] > 0), df['down_move'], 0)
# Smoothed values
df['atr'] = df['tr'].rolling(self.adx_period).mean()
df['+di'] = 100 * (df['+dm'].rolling(self.adx_period).mean() / df['atr'])
df['-di'] = 100 * (df['-dm'].rolling(self.adx_period).mean() / df['atr'])
# ADX
df['dx'] = 100 * abs(df['+di'] - df['-di']) / (df['+di'] + df['-di'])
df['adx'] = df['dx'].rolling(self.adx_period).mean()
return df[['adx', '+di', '-di']]
def is_trending(self, high: pd.Series, low: pd.Series, close: pd.Series) -> Dict:
"""Check if market is trending."""
adx_df = self.calculate_adx(high, low, close)
if adx_df['adx'].isna().iloc[-1]:
return {'trending': False, 'reason': 'Insufficient data'}
current_adx = adx_df['adx'].iloc[-1]
plus_di = adx_df['+di'].iloc[-1]
minus_di = adx_df['-di'].iloc[-1]
trending = current_adx > self.adx_threshold
direction = 'up' if plus_di > minus_di else 'down'
return {
'trending': trending,
'adx': current_adx,
'direction': direction if trending else 'none',
'strength': 'strong' if current_adx > 40 else ('moderate' if current_adx > 25 else 'weak')
}
# Demo
trend_filter = TrendFilter(adx_period=14, adx_threshold=25)
result = trend_filter.is_trending(high, low, close)
print(f"Trend Filter: {result}")
8.2 Range & Mean Reversion
Mean reversion strategies profit from price returning to equilibrium levels after temporary deviations.
class RangeDetector:
"""Detect ranging vs trending markets."""
def __init__(self, lookback: int = 20):
self.lookback = lookback
def detect_range(self, high: pd.Series, low: pd.Series,
close: pd.Series) -> Dict:
"""Detect if market is in a range."""
# Calculate range metrics
period_high = high.rolling(self.lookback).max().iloc[-1]
period_low = low.rolling(self.lookback).min().iloc[-1]
range_size = period_high - period_low
# Calculate ATR for comparison
tr = pd.concat([
high - low,
abs(high - close.shift(1)),
abs(low - close.shift(1))
], axis=1).max(axis=1)
atr = tr.rolling(14).mean().iloc[-1]
# Range ratio: low ratio = ranging, high ratio = trending
range_ratio = range_size / (atr * self.lookback)
# Current position within range
current = close.iloc[-1]
position_in_range = (current - period_low) / range_size if range_size > 0 else 0.5
is_ranging = range_ratio < 0.5
return {
'is_ranging': is_ranging,
'range_high': period_high,
'range_low': period_low,
'range_size': range_size,
'range_ratio': range_ratio,
'position_in_range': position_in_range,
'near_resistance': position_in_range > 0.8,
'near_support': position_in_range < 0.2
}
# Demo with ranging data
np.random.seed(123)
range_prices = 1.1000 + np.sin(np.linspace(0, 4*np.pi, 100)) * 0.01 + np.random.normal(0, 0.002, 100)
range_dates = pd.date_range('2024-01-01', periods=100, freq='D')
range_high = pd.Series(range_prices + np.random.uniform(0, 0.002, 100), index=range_dates)
range_low = pd.Series(range_prices - np.random.uniform(0, 0.002, 100), index=range_dates)
range_close = pd.Series(range_prices, index=range_dates)
detector = RangeDetector(lookback=20)
print(f"Range Detection: {detector.detect_range(range_high, range_low, range_close)}")
class MeanReversionStrategy:
"""Mean reversion strategy using Bollinger Bands and RSI."""
def __init__(self, bb_period: int = 20, bb_std: float = 2.0,
rsi_period: int = 14, oversold: float = 30, overbought: float = 70):
self.bb_period = bb_period
self.bb_std = bb_std
self.rsi_period = rsi_period
self.oversold = oversold
self.overbought = overbought
self.prices: pd.Series = pd.Series(dtype=float)
def set_data(self, prices: pd.Series) -> None:
"""Set price data."""
self.prices = prices
def calculate_indicators(self) -> pd.DataFrame:
"""Calculate Bollinger Bands and RSI."""
df = pd.DataFrame({'price': self.prices})
# Bollinger Bands
df['bb_middle'] = df['price'].rolling(self.bb_period).mean()
df['bb_std'] = df['price'].rolling(self.bb_period).std()
df['bb_upper'] = df['bb_middle'] + self.bb_std * df['bb_std']
df['bb_lower'] = df['bb_middle'] - self.bb_std * df['bb_std']
# RSI
delta = df['price'].diff()
gain = (delta.where(delta > 0, 0)).rolling(self.rsi_period).mean()
loss = (-delta.where(delta < 0, 0)).rolling(self.rsi_period).mean()
rs = gain / loss
df['rsi'] = 100 - (100 / (1 + rs))
# %B indicator (position within bands)
df['percent_b'] = (df['price'] - df['bb_lower']) / (df['bb_upper'] - df['bb_lower'])
return df
def calculate_signals(self) -> pd.DataFrame:
"""Calculate mean reversion signals."""
df = self.calculate_indicators()
df['signal'] = 0
# Buy when oversold (price below lower band AND RSI oversold)
df.loc[(df['price'] < df['bb_lower']) & (df['rsi'] < self.oversold), 'signal'] = 1
# Sell when overbought (price above upper band AND RSI overbought)
df.loc[(df['price'] > df['bb_upper']) & (df['rsi'] > self.overbought), 'signal'] = -1
return df
def get_current_signal(self) -> Dict:
"""Get current signal."""
df = self.calculate_indicators()
latest = df.iloc[-1]
# Determine condition
if latest['price'] < latest['bb_lower'] and latest['rsi'] < self.oversold:
signal = 'buy'
condition = 'oversold'
elif latest['price'] > latest['bb_upper'] and latest['rsi'] > self.overbought:
signal = 'sell'
condition = 'overbought'
elif latest['percent_b'] < 0.2:
signal = 'watch_buy'
condition = 'approaching_oversold'
elif latest['percent_b'] > 0.8:
signal = 'watch_sell'
condition = 'approaching_overbought'
else:
signal = 'neutral'
condition = 'normal'
return {
'signal': signal,
'condition': condition,
'price': latest['price'],
'bb_upper': latest['bb_upper'],
'bb_lower': latest['bb_lower'],
'rsi': latest['rsi'],
'percent_b': latest['percent_b']
}
# Demo
mr_strategy = MeanReversionStrategy()
mr_strategy.set_data(range_close)
print("Mean Reversion Strategy:")
print(f"Current Signal: {mr_strategy.get_current_signal()}")
8.3 Carry Trade
Carry trade profits from interest rate differentials by going long high-yield currencies and short low-yield currencies.
Carry Trade Mechanics
| Component | Description |
|---|---|
| Interest Differential | Profit from holding high-yield vs low-yield |
| Swap Points | Daily credit/debit based on rate differential |
| Risk | Currency depreciation can wipe out carry gains |
| Best Conditions | Low volatility, risk-on environment |
class CarryTradeCalculator:
"""Calculate carry trade opportunities and returns."""
def __init__(self):
self.rates: Dict[str, float] = {}
self.prices: Dict[str, pd.Series] = {}
def set_rate(self, currency: str, rate: float) -> None:
"""Set interest rate for currency."""
self.rates[currency] = rate
def set_price(self, pair: str, prices: pd.Series) -> None:
"""Set price data for currency pair."""
self.prices[pair] = prices
def calculate_carry(self, long_currency: str, short_currency: str) -> Dict:
"""Calculate annual carry for a trade."""
if long_currency not in self.rates or short_currency not in self.rates:
return {'carry': 0, 'error': 'Missing rate data'}
long_rate = self.rates[long_currency]
short_rate = self.rates[short_currency]
carry = long_rate - short_rate
daily_carry = carry / 365
return {
'long_currency': long_currency,
'short_currency': short_currency,
'long_rate': long_rate,
'short_rate': short_rate,
'annual_carry': carry,
'daily_carry': daily_carry,
'monthly_carry': carry / 12
}
def get_best_carry_pairs(self) -> List[Dict]:
"""Find best carry trade opportunities."""
pairs = []
currencies = list(self.rates.keys())
for i, c1 in enumerate(currencies):
for c2 in currencies[i+1:]:
if self.rates[c1] > self.rates[c2]:
carry = self.calculate_carry(c1, c2)
else:
carry = self.calculate_carry(c2, c1)
pairs.append(carry)
return sorted(pairs, key=lambda x: x['annual_carry'], reverse=True)
def calculate_total_return(self, pair: str, long_currency: str,
short_currency: str, days: int = 30) -> Dict:
"""Calculate total return (carry + price change)."""
carry = self.calculate_carry(long_currency, short_currency)
if pair not in self.prices or len(self.prices[pair]) < days:
return {'error': 'Insufficient price data'}
prices = self.prices[pair]
price_return = (prices.iloc[-1] / prices.iloc[-days] - 1) * 100
carry_return = carry['annual_carry'] * days / 365
# Adjust direction based on pair convention
if pair.startswith(long_currency):
total_return = price_return + carry_return
else:
total_return = -price_return + carry_return
return {
'pair': pair,
'days': days,
'price_return': price_return,
'carry_return': carry_return,
'total_return': total_return,
'carry_helped': carry_return > 0
}
# Demo
carry_calc = CarryTradeCalculator()
# Set interest rates
carry_calc.set_rate('USD', 5.25)
carry_calc.set_rate('EUR', 4.50)
carry_calc.set_rate('JPY', 0.10)
carry_calc.set_rate('AUD', 4.35)
carry_calc.set_rate('CHF', 1.75)
print("Best Carry Pairs:")
for pair in carry_calc.get_best_carry_pairs()[:3]:
print(f" Long {pair['long_currency']}/Short {pair['short_currency']}: {pair['annual_carry']:.2f}%")
class CarryTradeStrategy:
"""Full carry trade strategy with risk management."""
def __init__(self, min_carry: float = 2.0, max_volatility: float = 15.0):
self.min_carry = min_carry # Minimum carry to consider
self.max_volatility = max_volatility # Maximum acceptable volatility
self.calculator = CarryTradeCalculator()
self.volatility: Dict[str, float] = {}
self.vix: float = 0
def set_rate(self, currency: str, rate: float) -> None:
"""Set interest rate."""
self.calculator.set_rate(currency, rate)
def set_volatility(self, pair: str, vol: float) -> None:
"""Set annualized volatility for pair."""
self.volatility[pair] = vol
def set_vix(self, vix: float) -> None:
"""Set current VIX level."""
self.vix = vix
def risk_on_environment(self) -> bool:
"""Check if risk environment supports carry."""
return self.vix < 20
def calculate_risk_adjusted_carry(self, long_curr: str, short_curr: str,
pair: str) -> Dict:
"""Calculate carry adjusted for volatility risk."""
carry = self.calculator.calculate_carry(long_curr, short_curr)
vol = self.volatility.get(pair, 10.0)
# Sharpe-like ratio: carry / volatility
carry_ratio = carry['annual_carry'] / vol if vol > 0 else 0
return {
**carry,
'pair': pair,
'volatility': vol,
'carry_ratio': carry_ratio,
'attractive': carry['annual_carry'] >= self.min_carry and vol <= self.max_volatility
}
def generate_signals(self) -> List[Dict]:
"""Generate carry trade signals."""
if not self.risk_on_environment():
return [{'signal': 'risk_off', 'reason': f'VIX at {self.vix}, avoid carry'}]
signals = []
best_pairs = self.calculator.get_best_carry_pairs()
for pair_info in best_pairs:
# Construct pair name
pair = f"{pair_info['long_currency']}{pair_info['short_currency']}"
if pair_info['annual_carry'] >= self.min_carry:
vol = self.volatility.get(pair, 10.0)
if vol <= self.max_volatility:
signals.append({
'signal': 'carry_long',
'pair': pair,
'carry': pair_info['annual_carry'],
'volatility': vol,
'carry_ratio': pair_info['annual_carry'] / vol,
'confidence': 'high' if pair_info['annual_carry'] / vol > 0.5 else 'medium'
})
return sorted(signals, key=lambda x: x.get('carry_ratio', 0), reverse=True)
# Demo
carry_strategy = CarryTradeStrategy(min_carry=3.0, max_volatility=12.0)
# Set rates
carry_strategy.set_rate('USD', 5.25)
carry_strategy.set_rate('EUR', 4.50)
carry_strategy.set_rate('JPY', 0.10)
carry_strategy.set_rate('AUD', 4.35)
# Set volatilities
carry_strategy.set_volatility('USDJPY', 8.5)
carry_strategy.set_volatility('AUDJPY', 11.0)
carry_strategy.set_volatility('EURJPY', 9.0)
# Set VIX
carry_strategy.set_vix(16.5)
print("Carry Trade Signals:")
for signal in carry_strategy.generate_signals():
print(f" {signal}")
8.4 News Trading
News trading strategies capitalize on high-impact economic releases and central bank decisions.
class NewsEvent:
"""Represents a high-impact news event."""
def __init__(self, name: str, currency: str, release_time: datetime,
forecast: float, previous: float, impact: str = 'high'):
self.name = name
self.currency = currency
self.release_time = release_time
self.forecast = forecast
self.previous = previous
self.impact = impact
self.actual: Optional[float] = None
def set_actual(self, actual: float) -> None:
"""Set actual release value."""
self.actual = actual
@property
def surprise(self) -> Optional[float]:
"""Calculate surprise (actual - forecast)."""
if self.actual is not None:
return self.actual - self.forecast
return None
@property
def surprise_pct(self) -> Optional[float]:
"""Calculate surprise as percentage."""
if self.actual is not None and self.forecast != 0:
return (self.actual - self.forecast) / abs(self.forecast) * 100
return None
class NewsTrader:
"""News trading strategy implementation."""
# Expected impact of surprise on currency (pips per 1% surprise)
EVENT_SENSITIVITY = {
'NFP': 30,
'CPI': 25,
'GDP': 20,
'Rate Decision': 50,
'Retail Sales': 15,
'PMI': 10
}
def __init__(self):
self.events: List[NewsEvent] = []
self.historical_reactions: List[Dict] = []
def add_event(self, event: NewsEvent) -> None:
"""Add upcoming or past event."""
self.events.append(event)
def record_reaction(self, event_name: str, currency: str,
surprise_pct: float, price_move_pips: float) -> None:
"""Record historical price reaction to event."""
self.historical_reactions.append({
'event': event_name,
'currency': currency,
'surprise_pct': surprise_pct,
'price_move': price_move_pips,
'sensitivity': price_move_pips / surprise_pct if surprise_pct != 0 else 0
})
def estimate_move(self, event: NewsEvent, actual: float) -> Dict:
"""Estimate expected price move based on surprise."""
event.set_actual(actual)
surprise_pct = event.surprise_pct
if surprise_pct is None:
return {'error': 'No surprise calculated'}
# Get sensitivity for this event type
sensitivity = self.EVENT_SENSITIVITY.get(event.name, 15)
# Check historical reactions for this specific event
hist = [r for r in self.historical_reactions if r['event'] == event.name]
if hist:
sensitivity = np.mean([r['sensitivity'] for r in hist])
expected_move = surprise_pct * sensitivity
return {
'event': event.name,
'currency': event.currency,
'actual': actual,
'forecast': event.forecast,
'surprise_pct': surprise_pct,
'expected_move_pips': expected_move,
'direction': 'bullish' if expected_move > 0 else 'bearish',
'magnitude': 'large' if abs(expected_move) > 50 else ('medium' if abs(expected_move) > 20 else 'small')
}
def straddle_setup(self, event: NewsEvent, entry_distance_pips: float = 20) -> Dict:
"""Set up a straddle trade before news release."""
sensitivity = self.EVENT_SENSITIVITY.get(event.name, 15)
# Estimate potential move based on typical surprise
typical_surprise = 2.0 # Assume 2% typical surprise
expected_move = typical_surprise * sensitivity
return {
'strategy': 'straddle',
'event': event.name,
'release_time': event.release_time,
'buy_stop_distance': entry_distance_pips,
'sell_stop_distance': entry_distance_pips,
'target_pips': expected_move,
'stop_loss_pips': entry_distance_pips * 1.5,
'risk_reward': expected_move / (entry_distance_pips * 1.5),
'recommendation': 'trade' if expected_move / (entry_distance_pips * 1.5) > 1.5 else 'skip'
}
# Demo
news_trader = NewsTrader()
# Create NFP event
nfp = NewsEvent(
name='NFP',
currency='USD',
release_time=datetime(2024, 5, 3, 8, 30),
forecast=240,
previous=303
)
news_trader.add_event(nfp)
# Record some historical reactions
news_trader.record_reaction('NFP', 'USD', 5.0, 45)
news_trader.record_reaction('NFP', 'USD', -3.0, -30)
news_trader.record_reaction('NFP', 'USD', 10.0, 85)
print("Straddle Setup:")
print(news_trader.straddle_setup(nfp))
print("\nAfter Release (Actual = 275):")
print(news_trader.estimate_move(nfp, 275))
class FadeStrategy:
"""Fade (counter-trend) strategy after news spikes."""
def __init__(self, fade_threshold_pips: float = 50,
wait_minutes: int = 15):
self.fade_threshold = fade_threshold_pips
self.wait_minutes = wait_minutes
def analyze_spike(self, pre_news_price: float, spike_price: float,
current_price: float, pip_value: float = 0.0001) -> Dict:
"""Analyze post-news spike for fade opportunity."""
spike_pips = (spike_price - pre_news_price) / pip_value
current_pips = (current_price - pre_news_price) / pip_value
# Calculate retracement
if spike_pips != 0:
retracement = 1 - (current_pips / spike_pips)
else:
retracement = 0
return {
'spike_pips': spike_pips,
'current_from_pre': current_pips,
'retracement': retracement * 100,
'spike_direction': 'up' if spike_pips > 0 else 'down',
'extended': abs(spike_pips) > self.fade_threshold
}
def generate_fade_signal(self, pre_news: float, spike: float,
current: float, pip_value: float = 0.0001) -> Dict:
"""Generate fade trade signal."""
analysis = self.analyze_spike(pre_news, spike, current, pip_value)
if not analysis['extended']:
return {'signal': 'no_trade', 'reason': 'Spike not extended enough'}
# Fade if retracement is small (spike still extended)
if analysis['retracement'] < 25:
signal = 'sell' if analysis['spike_direction'] == 'up' else 'buy'
target = pre_news + (spike - pre_news) * 0.5 # Target 50% retracement
stop = spike + (spike - pre_news) * 0.2 # Stop beyond spike
return {
'signal': signal,
'entry': current,
'target': target,
'stop': stop,
'risk_pips': abs(stop - current) / pip_value,
'reward_pips': abs(target - current) / pip_value,
'reason': f'Fade extended {analysis["spike_direction"]} spike'
}
return {'signal': 'no_trade', 'reason': 'Already retracing'}
# Demo
fade = FadeStrategy(fade_threshold_pips=40)
# Scenario: NFP spike up 60 pips, now only retraced 10 pips
pre_news = 1.1000
spike = 1.1060 # 60 pip spike up
current = 1.1050 # Only 10 pip retracement
print("Fade Analysis:")
print(fade.analyze_spike(pre_news, spike, current))
print("\nFade Signal:")
print(fade.generate_fade_signal(pre_news, spike, current))
Exercises
Exercise 1: Triple MA Strategy (Guided)
Complete the TripleMAStrategy class that uses three moving averages for trend confirmation.
class TripleMAStrategy:
"""Triple moving average trend following strategy."""
def __init__(self, fast: int = 10, medium: int = 20, slow: int = 50):
self.fast = fast
self.medium = medium
self.slow = slow
self.prices: pd.Series = pd.Series(dtype=float)
def set_data(self, prices: pd.Series) -> None:
"""Set price data."""
self.prices = prices
def calculate_mas(self) -> pd.DataFrame:
"""Calculate all three MAs."""
df = pd.DataFrame({'price': self.prices})
df['fast_ma'] = df['price'].rolling(self.fast).______
df['medium_ma'] = df['price'].rolling(self.medium).mean()
df['slow_ma'] = df['price'].rolling(self.slow).mean()
return df
def get_alignment(self) -> str:
"""Check MA alignment."""
df = self.calculate_mas()
latest = df.iloc[-1]
if latest['fast_ma'] > latest['medium_ma'] > latest['______']:
return 'bullish'
elif latest['fast_ma'] < latest['medium_ma'] < latest['slow_ma']:
return 'bearish'
return 'mixed'
def generate_signal(self) -> Dict:
"""Generate trading signal."""
alignment = self.get_alignment()
df = self.calculate_mas()
# Check for pullback to medium MA in trending market
latest = df.iloc[-1]
near_medium = abs(latest['price'] - latest['medium_ma']) / latest['price'] < 0.005
if alignment == 'bullish' and near_medium:
return {'signal': '______', 'reason': 'Bullish alignment + pullback'}
elif alignment == 'bearish' and near_medium:
return {'signal': 'sell', 'reason': 'Bearish alignment + pullback'}
elif alignment in ['bullish', 'bearish']:
return {'signal': 'hold', 'reason': f'{alignment} but no pullback'}
return {'signal': 'neutral', 'reason': 'No clear trend'}
# Test
triple_ma = TripleMAStrategy()
triple_ma.set_data(prices)
print(f"Alignment: {triple_ma.get_alignment()}")
print(f"Signal: {triple_ma.generate_signal()}")
Solution 1
class TripleMAStrategy:
def __init__(self, fast: int = 10, medium: int = 20, slow: int = 50):
self.fast = fast
self.medium = medium
self.slow = slow
self.prices: pd.Series = pd.Series(dtype=float)
def set_data(self, prices: pd.Series) -> None:
self.prices = prices
def calculate_mas(self) -> pd.DataFrame:
df = pd.DataFrame({'price': self.prices})
df['fast_ma'] = df['price'].rolling(self.fast).mean()
df['medium_ma'] = df['price'].rolling(self.medium).mean()
df['slow_ma'] = df['price'].rolling(self.slow).mean()
return df
def get_alignment(self) -> str:
df = self.calculate_mas()
latest = df.iloc[-1]
if latest['fast_ma'] > latest['medium_ma'] > latest['slow_ma']:
return 'bullish'
elif latest['fast_ma'] < latest['medium_ma'] < latest['slow_ma']:
return 'bearish'
return 'mixed'
def generate_signal(self) -> Dict:
alignment = self.get_alignment()
df = self.calculate_mas()
latest = df.iloc[-1]
near_medium = abs(latest['price'] - latest['medium_ma']) / latest['price'] < 0.005
if alignment == 'bullish' and near_medium:
return {'signal': 'buy', 'reason': 'Bullish alignment + pullback'}
elif alignment == 'bearish' and near_medium:
return {'signal': 'sell', 'reason': 'Bearish alignment + pullback'}
elif alignment in ['bullish', 'bearish']:
return {'signal': 'hold', 'reason': f'{alignment} but no pullback'}
return {'signal': 'neutral', 'reason': 'No clear trend'}
Exercise 2: RSI Divergence Detector (Guided)
Complete the RSIDivergence class that detects bullish and bearish RSI divergences.
class RSIDivergence:
"""Detect RSI divergences for mean reversion signals."""
def __init__(self, rsi_period: int = 14, lookback: int = 20):
self.rsi_period = rsi_period
self.lookback = lookback
self.prices: pd.Series = pd.Series(dtype=float)
def set_data(self, prices: pd.Series) -> None:
"""Set price data."""
self.prices = prices
def calculate_rsi(self) -> pd.Series:
"""Calculate RSI."""
delta = self.prices.diff()
gain = (delta.where(delta > 0, 0)).rolling(self.rsi_period).______
loss = (-delta.where(delta < 0, 0)).rolling(self.rsi_period).mean()
rs = gain / loss
return 100 - (100 / (1 + rs))
def find_divergence(self) -> Dict:
"""Find bullish or bearish divergence."""
rsi = self.calculate_rsi()
prices = self.prices[-self.lookback:]
rsi_recent = rsi[-self.lookback:]
# Find local lows for bullish divergence
price_low_idx = prices.______
rsi_at_price_low = rsi_recent.iloc[prices.index.get_loc(price_low_idx) - (len(self.prices) - self.lookback)]
current_price = prices.iloc[-1]
current_rsi = rsi_recent.iloc[-1]
# Bullish divergence: price makes lower low, RSI makes higher low
if current_price < prices[price_low_idx] and current_rsi > rsi_at_price_low:
return {
'divergence': 'bullish',
'signal': 'buy',
'price_low': prices[price_low_idx],
'current_price': current_price,
'rsi_at_low': rsi_at_price_low,
'current_rsi': current_rsi
}
# Check bearish (price higher high, RSI lower high)
price_high_idx = prices.idxmax()
rsi_at_price_high = rsi_recent.iloc[prices.index.get_loc(price_high_idx) - (len(self.prices) - self.lookback)]
if current_price > prices[price_high_idx] and current_rsi < rsi_at_price_high:
return {
'divergence': '______',
'signal': 'sell',
'price_high': prices[price_high_idx],
'current_price': current_price,
'rsi_at_high': rsi_at_price_high,
'current_rsi': current_rsi
}
return {'divergence': 'none', 'signal': 'neutral'}
# Test
div_detector = RSIDivergence()
div_detector.set_data(range_close)
print(f"Divergence: {div_detector.find_divergence()}")
Solution 2
class RSIDivergence:
def __init__(self, rsi_period: int = 14, lookback: int = 20):
self.rsi_period = rsi_period
self.lookback = lookback
self.prices: pd.Series = pd.Series(dtype=float)
def set_data(self, prices: pd.Series) -> None:
self.prices = prices
def calculate_rsi(self) -> pd.Series:
delta = self.prices.diff()
gain = (delta.where(delta > 0, 0)).rolling(self.rsi_period).mean()
loss = (-delta.where(delta < 0, 0)).rolling(self.rsi_period).mean()
rs = gain / loss
return 100 - (100 / (1 + rs))
def find_divergence(self) -> Dict:
rsi = self.calculate_rsi()
prices = self.prices[-self.lookback:]
rsi_recent = rsi[-self.lookback:]
price_low_idx = prices.idxmin()
# ... rest of implementation
# Bearish divergence check
if current_price > prices[price_high_idx] and current_rsi < rsi_at_price_high:
return {
'divergence': 'bearish',
'signal': 'sell',
...
}
Exercise 3: Carry Portfolio Builder (Guided)
Complete the CarryPortfolio class that builds a diversified carry trade portfolio.
class CarryPortfolio:
"""Build diversified carry trade portfolio."""
def __init__(self, max_positions: int = 5, min_carry: float = 2.0):
self.max_positions = max_positions
self.min_carry = min_carry
self.rates: Dict[str, float] = {}
self.positions: List[Dict] = []
def set_rate(self, currency: str, rate: float) -> None:
"""Set interest rate."""
self.rates[currency] = rate
def get_all_pairs(self) -> List[Dict]:
"""Get all possible carry pairs."""
pairs = []
currencies = list(self.rates.keys())
for i, c1 in enumerate(currencies):
for c2 in currencies[i+1:]:
carry = self.rates[c1] - self.rates[c2]
if abs(carry) >= self.min_carry:
if carry > 0:
pairs.append({'long': c1, 'short': c2, 'carry': carry})
else:
pairs.append({'long': c2, 'short': c1, 'carry': ______(carry)})
return sorted(pairs, key=lambda x: x['carry'], reverse=True)
def build_portfolio(self) -> List[Dict]:
"""Build diversified portfolio."""
all_pairs = self.get_all_pairs()
portfolio = []
used_currencies = set()
for pair in all_pairs:
if len(portfolio) >= self.max_positions:
break
# Check if currencies already used (for diversification)
if pair['long'] not in used_currencies or pair['______'] not in used_currencies:
portfolio.append(pair)
used_currencies.add(pair['long'])
used_currencies.add(pair['short'])
self.positions = portfolio
return portfolio
def portfolio_carry(self) -> float:
"""Calculate total portfolio carry."""
if not self.positions:
self.build_portfolio()
return sum(p['carry'] for p in self.positions) / len(self.positions) if self.positions else 0
# Test
portfolio = CarryPortfolio(max_positions=3, min_carry=2.0)
portfolio.set_rate('USD', 5.25)
portfolio.set_rate('EUR', 4.50)
portfolio.set_rate('JPY', 0.10)
portfolio.set_rate('AUD', 4.35)
portfolio.set_rate('CHF', 1.75)
print("Carry Portfolio:")
for pos in portfolio.build_portfolio():
print(f" Long {pos['long']}/Short {pos['short']}: {pos['carry']:.2f}%")
print(f"\nAverage Carry: {portfolio.portfolio_carry():.2f}%")
Solution 3
class CarryPortfolio:
def __init__(self, max_positions: int = 5, min_carry: float = 2.0):
self.max_positions = max_positions
self.min_carry = min_carry
self.rates: Dict[str, float] = {}
self.positions: List[Dict] = []
def set_rate(self, currency: str, rate: float) -> None:
self.rates[currency] = rate
def get_all_pairs(self) -> List[Dict]:
pairs = []
currencies = list(self.rates.keys())
for i, c1 in enumerate(currencies):
for c2 in currencies[i+1:]:
carry = self.rates[c1] - self.rates[c2]
if abs(carry) >= self.min_carry:
if carry > 0:
pairs.append({'long': c1, 'short': c2, 'carry': carry})
else:
pairs.append({'long': c2, 'short': c1, 'carry': abs(carry)})
return sorted(pairs, key=lambda x: x['carry'], reverse=True)
def build_portfolio(self) -> List[Dict]:
all_pairs = self.get_all_pairs()
portfolio = []
used_currencies = set()
for pair in all_pairs:
if len(portfolio) >= self.max_positions:
break
if pair['long'] not in used_currencies or pair['short'] not in used_currencies:
portfolio.append(pair)
used_currencies.add(pair['long'])
used_currencies.add(pair['short'])
self.positions = portfolio
return portfolio
Exercise 4: Complete Trend Following System (Open-ended)
Build a comprehensive trend following system that: - Uses multiple timeframe confirmation - Includes ADX trend filter - Implements trailing stops - Manages position sizing
# Exercise 4: Complete Trend Following System (Open-ended)
#
# Requirements:
# 1. Create class TrendFollowingSystem
# 2. Multiple MA periods (fast, medium, slow)
# 3. ADX filter (only trade when ADX > threshold)
# 4. ATR-based trailing stop
# 5. Position sizing based on account risk %
# 6. Generate signals with entry, stop, and position size
#
# Your implementation:
Solution 4
class TrendFollowingSystem:
def __init__(self, fast_ma=10, medium_ma=20, slow_ma=50, adx_threshold=25):
self.fast_ma = fast_ma
self.medium_ma = medium_ma
self.slow_ma = slow_ma
self.adx_threshold = adx_threshold
self.prices: pd.DataFrame = pd.DataFrame()
def set_data(self, high, low, close):
self.prices = pd.DataFrame({'high': high, 'low': low, 'close': close})
def calculate_indicators(self):
df = self.prices.copy()
df['fast'] = df['close'].rolling(self.fast_ma).mean()
df['medium'] = df['close'].rolling(self.medium_ma).mean()
df['slow'] = df['close'].rolling(self.slow_ma).mean()
# ATR
tr = pd.concat([df['high']-df['low'], abs(df['high']-df['close'].shift(1)),
abs(df['low']-df['close'].shift(1))], axis=1).max(axis=1)
df['atr'] = tr.rolling(14).mean()
# ADX (simplified)
df['adx'] = 30 # Placeholder
return df
def calculate_position_size(self, account: float, risk_pct: float,
stop_pips: float, pip_value: float):
risk_amount = account * risk_pct / 100
position = risk_amount / (stop_pips * pip_value)
return position
def generate_signal(self, account: float = 10000, risk_pct: float = 1.0):
df = self.calculate_indicators()
latest = df.iloc[-1]
# Check alignment and ADX
bullish = latest['fast'] > latest['medium'] > latest['slow']
bearish = latest['fast'] < latest['medium'] < latest['slow']
trending = latest['adx'] > self.adx_threshold
if bullish and trending:
stop = latest['close'] - 2 * latest['atr']
stop_pips = (latest['close'] - stop) / 0.0001
size = self.calculate_position_size(account, risk_pct, stop_pips, 10)
return {'signal': 'buy', 'entry': latest['close'], 'stop': stop, 'size': size}
elif bearish and trending:
stop = latest['close'] + 2 * latest['atr']
stop_pips = (stop - latest['close']) / 0.0001
size = self.calculate_position_size(account, risk_pct, stop_pips, 10)
return {'signal': 'sell', 'entry': latest['close'], 'stop': stop, 'size': size}
return {'signal': 'neutral'}
Exercise 5: Range Trading System (Open-ended)
Build a range trading system that: - Detects ranging markets - Identifies support/resistance levels - Uses oscillators for confirmation - Sets appropriate targets and stops
# Exercise 5: Range Trading System (Open-ended)
#
# Requirements:
# 1. Create class RangeTradingSystem
# 2. Detect ranging markets (low ADX or range detection)
# 3. Identify support/resistance from recent highs/lows
# 4. Use RSI or Stochastic for entry timing
# 5. Target opposite boundary of range
# 6. Stop outside range boundary
#
# Your implementation:
Solution 5
class RangeTradingSystem:
def __init__(self, lookback=20, rsi_period=14):
self.lookback = lookback
self.rsi_period = rsi_period
self.prices: pd.DataFrame = pd.DataFrame()
def set_data(self, high, low, close):
self.prices = pd.DataFrame({'high': high, 'low': low, 'close': close})
def detect_range(self):
resistance = self.prices['high'].rolling(self.lookback).max().iloc[-1]
support = self.prices['low'].rolling(self.lookback).min().iloc[-1]
range_size = resistance - support
# Check if price stayed in range
recent = self.prices[-self.lookback:]
in_range = all((recent['high'] <= resistance * 1.01) & (recent['low'] >= support * 0.99))
return {'ranging': in_range, 'resistance': resistance, 'support': support, 'range': range_size}
def calculate_rsi(self):
delta = self.prices['close'].diff()
gain = (delta.where(delta > 0, 0)).rolling(self.rsi_period).mean()
loss = (-delta.where(delta < 0, 0)).rolling(self.rsi_period).mean()
return 100 - (100 / (1 + gain/loss))
def generate_signal(self):
rng = self.detect_range()
if not rng['ranging']:
return {'signal': 'no_trade', 'reason': 'Not ranging'}
rsi = self.calculate_rsi().iloc[-1]
price = self.prices['close'].iloc[-1]
near_support = (price - rng['support']) / rng['range'] < 0.2
near_resistance = (rng['resistance'] - price) / rng['range'] < 0.2
if near_support and rsi < 30:
return {
'signal': 'buy',
'entry': price,
'target': rng['resistance'],
'stop': rng['support'] - rng['range'] * 0.1
}
elif near_resistance and rsi > 70:
return {
'signal': 'sell',
'entry': price,
'target': rng['support'],
'stop': rng['resistance'] + rng['range'] * 0.1
}
return {'signal': 'wait', 'reason': 'Not at range extremes'}
Exercise 6: Multi-Strategy Manager (Open-ended)
Build a strategy manager that: - Combines trend, range, carry, and news strategies - Allocates capital based on market regime - Manages overall portfolio risk - Generates consolidated signals
# Exercise 6: Multi-Strategy Manager (Open-ended)
#
# Requirements:
# 1. Create class MultiStrategyManager
# 2. Include: trend, range, carry, news strategies
# 3. Detect market regime (trending vs ranging)
# 4. Allocate capital based on regime
# 5. Set overall portfolio risk limits
# 6. Generate consolidated trade recommendations
#
# Your implementation:
Solution 6
class MultiStrategyManager:
def __init__(self, total_capital: float, max_risk_pct: float = 5.0):
self.capital = total_capital
self.max_risk = max_risk_pct
self.strategies = {}
self.allocations = {'trend': 0.3, 'range': 0.3, 'carry': 0.3, 'news': 0.1}
def add_strategy(self, name: str, strategy) -> None:
self.strategies[name] = strategy
def detect_regime(self, adx: float) -> str:
if adx > 25:
return 'trending'
return 'ranging'
def adjust_allocations(self, regime: str) -> None:
if regime == 'trending':
self.allocations = {'trend': 0.5, 'range': 0.1, 'carry': 0.3, 'news': 0.1}
else:
self.allocations = {'trend': 0.1, 'range': 0.5, 'carry': 0.3, 'news': 0.1}
def get_strategy_capital(self, strategy_name: str) -> float:
return self.capital * self.allocations.get(strategy_name, 0)
def generate_signals(self, regime: str = 'trending') -> List[Dict]:
self.adjust_allocations(regime)
all_signals = []
for name, strategy in self.strategies.items():
if hasattr(strategy, 'generate_signal'):
signal = strategy.generate_signal()
signal['strategy'] = name
signal['capital_allocated'] = self.get_strategy_capital(name)
all_signals.append(signal)
return [s for s in all_signals if s.get('signal') not in ['neutral', 'no_trade']]
def check_risk_limits(self, signals: List[Dict]) -> List[Dict]:
total_risk = sum(s.get('risk_pct', 1) for s in signals)
if total_risk > self.max_risk:
scale = self.max_risk / total_risk
for s in signals:
s['position_scale'] = scale
return signals
Module Project: Multi-Strategy Forex System
Build a production-ready system that combines all trading strategies.
class ForexTradingSystem:
"""
Production-ready multi-strategy forex trading system.
Integrates: Trend following, Mean reversion, Carry trade, News trading.
Features: Regime detection, Capital allocation, Risk management.
"""
def __init__(self, capital: float = 100000, max_risk_pct: float = 2.0):
self.capital = capital
self.max_risk_pct = max_risk_pct
# Initialize strategies
self.ma_strategy = MovingAverageCrossover()
self.mr_strategy = MeanReversionStrategy()
self.carry_strategy = CarryTradeStrategy()
# Market data
self.prices: Dict[str, pd.Series] = {}
self.rates: Dict[str, float] = {}
def load_price_data(self, pair: str, prices: pd.Series) -> None:
"""Load price data for a currency pair."""
self.prices[pair] = prices
def set_interest_rate(self, currency: str, rate: float) -> None:
"""Set interest rate for carry calculations."""
self.rates[currency] = rate
self.carry_strategy.set_rate(currency, rate)
def detect_regime(self, pair: str) -> str:
"""Detect market regime (trending vs ranging)."""
if pair not in self.prices:
return 'unknown'
prices = self.prices[pair]
# Simple regime detection using MA slope and range
ma20 = prices.rolling(20).mean()
ma_slope = (ma20.iloc[-1] - ma20.iloc[-10]) / ma20.iloc[-10] * 100
range_pct = (prices.rolling(20).max().iloc[-1] - prices.rolling(20).min().iloc[-1]) / prices.iloc[-1] * 100
if abs(ma_slope) > 1 and range_pct > 2:
return 'trending'
elif range_pct < 1.5:
return 'ranging'
return 'transitioning'
def get_trend_signals(self, pair: str) -> Dict:
"""Get trend following signals."""
if pair not in self.prices:
return {'signal': 'no_data'}
self.ma_strategy.set_data(self.prices[pair])
return self.ma_strategy.get_current_signal()
def get_mr_signals(self, pair: str) -> Dict:
"""Get mean reversion signals."""
if pair not in self.prices:
return {'signal': 'no_data'}
self.mr_strategy.set_data(self.prices[pair])
return self.mr_strategy.get_current_signal()
def get_carry_signals(self) -> List[Dict]:
"""Get carry trade signals."""
self.carry_strategy.set_vix(16) # Default low vol
return self.carry_strategy.generate_signals()
def generate_recommendations(self, pair: str) -> List[Dict]:
"""Generate all trading recommendations."""
recommendations = []
regime = self.detect_regime(pair)
# Trend following (prioritize in trending regime)
trend_signal = self.get_trend_signals(pair)
if trend_signal.get('signal') in ['long', 'short']:
recommendations.append({
'strategy': 'trend_following',
'pair': pair,
'direction': trend_signal['signal'],
'confidence': 'high' if regime == 'trending' else 'low',
'details': trend_signal
})
# Mean reversion (prioritize in ranging regime)
mr_signal = self.get_mr_signals(pair)
if mr_signal.get('signal') in ['buy', 'sell']:
recommendations.append({
'strategy': 'mean_reversion',
'pair': pair,
'direction': 'long' if mr_signal['signal'] == 'buy' else 'short',
'confidence': 'high' if regime == 'ranging' else 'low',
'details': mr_signal
})
# Carry trades
for carry in self.get_carry_signals():
if carry.get('signal') == 'carry_long':
recommendations.append({
'strategy': 'carry_trade',
'pair': carry.get('pair'),
'direction': 'long',
'confidence': carry.get('confidence', 'medium'),
'details': carry
})
return recommendations
def calculate_position_size(self, pair: str, stop_pips: float) -> float:
"""Calculate position size based on risk."""
risk_amount = self.capital * self.max_risk_pct / 100
pip_value = 10 # Standard lot pip value for major pairs
position_size = risk_amount / (stop_pips * pip_value)
return round(position_size, 2)
def print_dashboard(self, pair: str) -> None:
"""Print trading dashboard."""
print("\n" + "=" * 60)
print(" FOREX TRADING SYSTEM DASHBOARD")
print(f" {datetime.now().strftime('%Y-%m-%d %H:%M')}")
print("=" * 60)
# Market Regime
regime = self.detect_regime(pair)
print(f"\nMarket Regime: {regime.upper()}")
print(f"Primary Pair: {pair}")
# Strategy Signals
print("\n" + "-" * 40)
print("STRATEGY SIGNALS")
print("-" * 40)
print(f"\nTrend Following: {self.get_trend_signals(pair).get('signal', 'N/A')}")
print(f"Mean Reversion: {self.get_mr_signals(pair).get('signal', 'N/A')}")
carry_signals = self.get_carry_signals()
print(f"Carry Trades: {len(carry_signals)} opportunities")
# Recommendations
print("\n" + "-" * 40)
print("RECOMMENDATIONS")
print("-" * 40)
for i, rec in enumerate(self.generate_recommendations(pair), 1):
print(f"\n{i}. {rec['strategy'].upper()}")
print(f" Pair: {rec['pair']} | Direction: {rec['direction'].upper()}")
print(f" Confidence: {rec['confidence']}")
print("\n" + "=" * 60)
# Demo the system
np.random.seed(42)
dates = pd.date_range('2024-01-01', periods=100, freq='D')
system = ForexTradingSystem(capital=100000, max_risk_pct=1.0)
# Load price data
eurusd = pd.Series(1.0800 + np.cumsum(np.random.normal(0.0002, 0.005, 100)), index=dates)
usdjpy = pd.Series(150.00 + np.cumsum(np.random.normal(0.02, 0.5, 100)), index=dates)
system.load_price_data('EURUSD', eurusd)
system.load_price_data('USDJPY', usdjpy)
# Set interest rates
system.set_interest_rate('USD', 5.25)
system.set_interest_rate('EUR', 4.50)
system.set_interest_rate('JPY', 0.10)
system.set_interest_rate('AUD', 4.35)
# Set volatilities for carry strategy
system.carry_strategy.set_volatility('USDJPY', 9.0)
system.carry_strategy.set_volatility('AUDJPY', 11.0)
# Print dashboard
system.print_dashboard('EURUSD')
Key Takeaways
- Trend Following: MA crossovers and breakouts work best in trending markets; use ADX filter
- Mean Reversion: Range trading and Bollinger Band strategies work in low-volatility, ranging markets
- Carry Trade: Profit from interest rate differentials; works best in risk-on, low-volatility environments
- News Trading: Straddle for unknown direction, fade extended spikes; requires fast execution
- Regime Detection: Match strategy to market conditions; trend strategies fail in ranges and vice versa
- Position Sizing: Always size based on account risk %, not potential profit
- Multi-Strategy: Diversify across strategies that perform differently in various conditions
- Risk First: No strategy works all the time; proper risk management is essential
Next: Part 3 - Risk & Execution where we'll build comprehensive risk management and backtesting systems.
Module 9: Risk Management
Part 3: Risk & Execution
| Duration | Exercises |
|---|---|
| ~2.5 hours | 6 |
Learning Objectives
- Implement pip-based and ATR-based stop losses for forex
- Calculate tick-based risk and position sizing for futures
- Manage portfolio-level currency exposure and hedging
- Handle weekend gaps and overnight risk
Prerequisites
- Modules 1-8 (Forex/Futures fundamentals, strategies)
- Understanding of leverage and margin
- Python pandas proficiency
import pandas as pd
import numpy as np
from datetime import datetime, timedelta
from dataclasses import dataclass, field
from typing import List, Dict, Tuple, Optional
from enum import Enum
import warnings
warnings.filterwarnings('ignore')
9.1 Forex Risk Management
Forex risk management requires understanding pip values, proper stop placement, and currency correlation risks.
Pip Value Calculation
| Pair Type | Pip Size | Pip Value (Standard Lot) |
|---|---|---|
| XXX/USD | 0.0001 | $10 |
| USD/XXX | 0.0001 | $10 / exchange rate |
| XXX/JPY | 0.01 | ~$7-9 (varies) |
| Cross pairs | 0.0001 | Requires conversion |
class ForexRiskCalculator:
"""Calculate forex position risk and sizing."""
# Pip sizes by pair type
PIP_SIZES = {
'standard': 0.0001, # Most pairs
'jpy': 0.01, # JPY pairs
}
def __init__(self, account_currency: str = 'USD'):
self.account_currency = account_currency
self.exchange_rates: Dict[str, float] = {} # For conversion
def set_exchange_rate(self, pair: str, rate: float) -> None:
"""Set exchange rate for conversion."""
self.exchange_rates[pair] = rate
def get_pip_size(self, pair: str) -> float:
"""Get pip size for a currency pair."""
if 'JPY' in pair:
return self.PIP_SIZES['jpy']
return self.PIP_SIZES['standard']
def calculate_pip_value(self, pair: str, lot_size: float = 1.0) -> float:
"""Calculate pip value in account currency."""
pip_size = self.get_pip_size(pair)
units = lot_size * 100000 # Standard lot = 100,000 units
# Base pip value
pip_value = pip_size * units
# Convert to account currency
quote_currency = pair[-3:]
if quote_currency == self.account_currency:
return pip_value
elif quote_currency == 'JPY' and self.account_currency == 'USD':
usdjpy = self.exchange_rates.get('USDJPY', 150.0)
return pip_value / usdjpy
else:
# Try to find conversion rate
conversion_pair = f"{quote_currency}{self.account_currency}"
if conversion_pair in self.exchange_rates:
return pip_value * self.exchange_rates[conversion_pair]
reverse_pair = f"{self.account_currency}{quote_currency}"
if reverse_pair in self.exchange_rates:
return pip_value / self.exchange_rates[reverse_pair]
return pip_value # Default
def calculate_position_size(self, account_balance: float, risk_percent: float,
stop_loss_pips: float, pair: str) -> Dict:
"""Calculate position size based on risk."""
risk_amount = account_balance * (risk_percent / 100)
pip_value_per_lot = self.calculate_pip_value(pair, 1.0)
# Position size in lots
position_lots = risk_amount / (stop_loss_pips * pip_value_per_lot)
# Convert to units
position_units = position_lots * 100000
return {
'pair': pair,
'account_balance': account_balance,
'risk_percent': risk_percent,
'risk_amount': risk_amount,
'stop_loss_pips': stop_loss_pips,
'pip_value_per_lot': pip_value_per_lot,
'position_lots': round(position_lots, 2),
'position_units': int(position_units),
'mini_lots': round(position_lots * 10, 1),
'micro_lots': round(position_lots * 100, 0)
}
def calculate_risk_from_position(self, pair: str, position_lots: float,
stop_loss_pips: float, account_balance: float) -> Dict:
"""Calculate risk from an existing position."""
pip_value = self.calculate_pip_value(pair, position_lots)
risk_amount = stop_loss_pips * pip_value
risk_percent = (risk_amount / account_balance) * 100
return {
'pair': pair,
'position_lots': position_lots,
'stop_loss_pips': stop_loss_pips,
'risk_amount': risk_amount,
'risk_percent': risk_percent,
'acceptable': risk_percent <= 2.0
}
# Demo
forex_risk = ForexRiskCalculator(account_currency='USD')
forex_risk.set_exchange_rate('USDJPY', 150.50)
forex_risk.set_exchange_rate('EURUSD', 1.0850)
print("Pip Values (per standard lot):")
print(f" EURUSD: ${forex_risk.calculate_pip_value('EURUSD', 1.0):.2f}")
print(f" USDJPY: ${forex_risk.calculate_pip_value('USDJPY', 1.0):.2f}")
print("\nPosition Sizing (1% risk, 30 pip stop):")
sizing = forex_risk.calculate_position_size(
account_balance=10000,
risk_percent=1.0,
stop_loss_pips=30,
pair='EURUSD'
)
print(f" Risk Amount: ${sizing['risk_amount']:.2f}")
print(f" Position Size: {sizing['position_lots']:.2f} lots ({sizing['position_units']:,} units)")
class StopLossCalculator:
"""Calculate stop loss levels using various methods."""
def __init__(self):
self.prices: pd.DataFrame = pd.DataFrame()
def set_data(self, high: pd.Series, low: pd.Series, close: pd.Series) -> None:
"""Set OHLC data."""
self.prices = pd.DataFrame({'high': high, 'low': low, 'close': close})
def fixed_pip_stop(self, entry: float, direction: str,
stop_pips: float, pip_size: float = 0.0001) -> float:
"""Calculate fixed pip-based stop loss."""
stop_distance = stop_pips * pip_size
if direction == 'long':
return entry - stop_distance
else:
return entry + stop_distance
def calculate_atr(self, period: int = 14) -> pd.Series:
"""Calculate Average True Range."""
high = self.prices['high']
low = self.prices['low']
close = self.prices['close']
tr1 = high - low
tr2 = abs(high - close.shift(1))
tr3 = abs(low - close.shift(1))
tr = pd.concat([tr1, tr2, tr3], axis=1).max(axis=1)
return tr.rolling(period).mean()
def atr_stop(self, entry: float, direction: str,
atr_multiplier: float = 2.0, atr_period: int = 14) -> Dict:
"""Calculate ATR-based stop loss."""
atr = self.calculate_atr(atr_period)
current_atr = atr.iloc[-1]
stop_distance = current_atr * atr_multiplier
if direction == 'long':
stop_price = entry - stop_distance
else:
stop_price = entry + stop_distance
return {
'stop_price': stop_price,
'atr': current_atr,
'stop_distance': stop_distance,
'multiplier': atr_multiplier
}
def swing_stop(self, direction: str, lookback: int = 10) -> Dict:
"""Calculate stop based on recent swing high/low."""
recent = self.prices.tail(lookback)
if direction == 'long':
swing_low = recent['low'].min()
swing_idx = recent['low'].idxmin()
stop_price = swing_low
else:
swing_high = recent['high'].max()
swing_idx = recent['high'].idxmax()
stop_price = swing_high
return {
'stop_price': stop_price,
'swing_date': swing_idx,
'lookback': lookback
}
def trailing_stop(self, entry: float, current_price: float,
direction: str, trail_pips: float,
current_stop: float = None,
pip_size: float = 0.0001) -> Dict:
"""Calculate trailing stop level."""
trail_distance = trail_pips * pip_size
if direction == 'long':
new_stop = current_price - trail_distance
if current_stop is not None:
new_stop = max(new_stop, current_stop)
triggered = current_price <= new_stop
else:
new_stop = current_price + trail_distance
if current_stop is not None:
new_stop = min(new_stop, current_stop)
triggered = current_price >= new_stop
profit_pips = abs(current_price - entry) / pip_size
return {
'stop_price': new_stop,
'current_price': current_price,
'profit_pips': profit_pips,
'triggered': triggered
}
# Demo
np.random.seed(42)
dates = pd.date_range('2024-01-01', periods=60, freq='D')
close = pd.Series(1.0800 + np.cumsum(np.random.normal(0.0002, 0.005, 60)), index=dates)
high = close + np.random.uniform(0.001, 0.003, 60)
low = close - np.random.uniform(0.001, 0.003, 60)
stop_calc = StopLossCalculator()
stop_calc.set_data(high, low, close)
entry = 1.0900
print(f"Entry Price: {entry}")
print(f"\nFixed 30-pip stop (long): {stop_calc.fixed_pip_stop(entry, 'long', 30):.5f}")
print(f"\nATR Stop (2x): {stop_calc.atr_stop(entry, 'long', 2.0)}")
print(f"\nSwing Stop: {stop_calc.swing_stop('long', 10)}")
9.2 Futures Risk Management
Futures risk is calculated based on tick values, contract specifications, and notional exposure.
Common Futures Contract Specifications
| Contract | Symbol | Tick Size | Tick Value | Margin |
|---|---|---|---|---|
| E-mini S&P 500 | ES | 0.25 | $12.50 | ~$13,200 |
| E-mini Nasdaq | NQ | 0.25 | $5.00 | ~$17,600 |
| Crude Oil | CL | 0.01 | $10.00 | ~$6,500 |
| Gold | GC | 0.10 | $10.00 | ~$9,000 |
| Euro FX | 6E | 0.0001 | $12.50 | ~$2,500 |
@dataclass
class FuturesContract:
"""Futures contract specifications."""
symbol: str
name: str
tick_size: float
tick_value: float
contract_size: float
initial_margin: float
maintenance_margin: float
@property
def point_value(self) -> float:
"""Value of 1 full point move."""
return self.tick_value / self.tick_size
class FuturesRiskCalculator:
"""Calculate futures position risk and sizing."""
# Standard contract specs
CONTRACTS = {
'ES': FuturesContract('ES', 'E-mini S&P 500', 0.25, 12.50, 50, 13200, 12000),
'NQ': FuturesContract('NQ', 'E-mini Nasdaq', 0.25, 5.00, 20, 17600, 16000),
'CL': FuturesContract('CL', 'Crude Oil', 0.01, 10.00, 1000, 6500, 5900),
'GC': FuturesContract('GC', 'Gold', 0.10, 10.00, 100, 9000, 8200),
'6E': FuturesContract('6E', 'Euro FX', 0.0001, 12.50, 125000, 2500, 2275),
'ZB': FuturesContract('ZB', 'T-Bond', 1/32, 31.25, 100000, 4400, 4000),
}
def __init__(self):
self.positions: List[Dict] = []
def get_contract(self, symbol: str) -> Optional[FuturesContract]:
"""Get contract specifications."""
return self.CONTRACTS.get(symbol)
def calculate_tick_risk(self, symbol: str, stop_ticks: float,
num_contracts: int = 1) -> Dict:
"""Calculate risk based on tick distance."""
contract = self.get_contract(symbol)
if not contract:
return {'error': f'Unknown contract: {symbol}'}
risk_per_contract = stop_ticks * contract.tick_value
total_risk = risk_per_contract * num_contracts
return {
'symbol': symbol,
'contracts': num_contracts,
'stop_ticks': stop_ticks,
'risk_per_contract': risk_per_contract,
'total_risk': total_risk,
'tick_value': contract.tick_value
}
def calculate_point_risk(self, symbol: str, entry: float, stop: float,
num_contracts: int = 1) -> Dict:
"""Calculate risk based on price levels."""
contract = self.get_contract(symbol)
if not contract:
return {'error': f'Unknown contract: {symbol}'}
point_distance = abs(entry - stop)
tick_distance = point_distance / contract.tick_size
return self.calculate_tick_risk(symbol, tick_distance, num_contracts)
def calculate_position_size(self, symbol: str, account_balance: float,
risk_percent: float, stop_ticks: float) -> Dict:
"""Calculate position size based on risk."""
contract = self.get_contract(symbol)
if not contract:
return {'error': f'Unknown contract: {symbol}'}
risk_amount = account_balance * (risk_percent / 100)
risk_per_contract = stop_ticks * contract.tick_value
max_contracts = int(risk_amount / risk_per_contract)
# Check margin requirements
margin_required = max_contracts * contract.initial_margin
margin_limited = margin_required > account_balance * 0.5 # Use max 50% for margin
if margin_limited:
margin_contracts = int((account_balance * 0.5) / contract.initial_margin)
max_contracts = min(max_contracts, margin_contracts)
return {
'symbol': symbol,
'account_balance': account_balance,
'risk_percent': risk_percent,
'risk_amount': risk_amount,
'stop_ticks': stop_ticks,
'risk_per_contract': risk_per_contract,
'max_contracts': max_contracts,
'margin_required': max_contracts * contract.initial_margin,
'margin_limited': margin_limited
}
def calculate_notional_value(self, symbol: str, price: float,
num_contracts: int = 1) -> float:
"""Calculate notional value of position."""
contract = self.get_contract(symbol)
if not contract:
return 0.0
return price * contract.contract_size * num_contracts
def calculate_leverage(self, symbol: str, price: float,
num_contracts: int, account_balance: float) -> float:
"""Calculate effective leverage."""
notional = self.calculate_notional_value(symbol, price, num_contracts)
return notional / account_balance if account_balance > 0 else 0
# Demo
futures_risk = FuturesRiskCalculator()
print("Futures Risk Calculations:")
print("\nES (E-mini S&P 500):")
es_risk = futures_risk.calculate_tick_risk('ES', stop_ticks=40, num_contracts=2)
print(f" 40 tick stop with 2 contracts: ${es_risk['total_risk']:.2f} risk")
print("\nPosition Sizing (1% risk, 20 tick stop):")
sizing = futures_risk.calculate_position_size('ES', account_balance=50000,
risk_percent=1.0, stop_ticks=20)
print(f" Max Contracts: {sizing['max_contracts']}")
print(f" Margin Required: ${sizing['margin_required']:,.2f}")
print("\nNotional & Leverage:")
notional = futures_risk.calculate_notional_value('ES', 5000, 2)
leverage = futures_risk.calculate_leverage('ES', 5000, 2, 50000)
print(f" Notional Value (2 ES @ 5000): ${notional:,.2f}")
print(f" Effective Leverage: {leverage:.1f}x")
9.3 Portfolio Risk
Managing portfolio-level risk requires understanding currency exposure, correlations, and hedging strategies.
class CurrencyExposureCalculator:
"""Calculate and manage currency exposure across portfolio."""
def __init__(self, base_currency: str = 'USD'):
self.base_currency = base_currency
self.positions: List[Dict] = []
def add_position(self, pair: str, direction: str,
units: float, entry_price: float) -> None:
"""Add a forex position."""
base = pair[:3]
quote = pair[3:]
# Calculate exposure
if direction == 'long':
base_exposure = units
quote_exposure = -units * entry_price
else:
base_exposure = -units
quote_exposure = units * entry_price
self.positions.append({
'pair': pair,
'direction': direction,
'units': units,
'entry_price': entry_price,
'base_currency': base,
'quote_currency': quote,
'base_exposure': base_exposure,
'quote_exposure': quote_exposure
})
def get_net_exposure(self) -> Dict[str, float]:
"""Calculate net exposure by currency."""
exposure = {}
for pos in self.positions:
base = pos['base_currency']
quote = pos['quote_currency']
exposure[base] = exposure.get(base, 0) + pos['base_exposure']
exposure[quote] = exposure.get(quote, 0) + pos['quote_exposure']
return exposure
def check_concentration(self, max_single_currency_pct: float = 30) -> Dict:
"""Check for currency concentration risk."""
exposure = self.get_net_exposure()
total_exposure = sum(abs(v) for v in exposure.values())
if total_exposure == 0:
return {'concentrated': False, 'details': {}}
concentration = {}
warnings = []
for currency, exp in exposure.items():
pct = abs(exp) / total_exposure * 100
concentration[currency] = pct
if pct > max_single_currency_pct:
warnings.append(f"{currency}: {pct:.1f}% (max {max_single_currency_pct}%)")
return {
'concentrated': len(warnings) > 0,
'warnings': warnings,
'concentration': concentration
}
def suggest_hedge(self, currency: str, hedge_pct: float = 50) -> Dict:
"""Suggest hedge for a currency exposure."""
exposure = self.get_net_exposure()
current_exposure = exposure.get(currency, 0)
if current_exposure == 0:
return {'hedge_needed': False}
hedge_amount = abs(current_exposure) * (hedge_pct / 100)
# Determine hedge direction
if current_exposure > 0:
hedge_direction = 'sell'
else:
hedge_direction = 'buy'
# Suggest hedge pair (vs USD)
if currency != self.base_currency:
hedge_pair = f"{currency}{self.base_currency}"
else:
hedge_pair = 'N/A (base currency)'
return {
'hedge_needed': True,
'currency': currency,
'current_exposure': current_exposure,
'hedge_pair': hedge_pair,
'hedge_direction': hedge_direction,
'hedge_amount': hedge_amount,
'remaining_exposure': current_exposure - (hedge_amount if current_exposure > 0 else -hedge_amount)
}
# Demo
exposure_calc = CurrencyExposureCalculator()
# Add positions
exposure_calc.add_position('EURUSD', 'long', 100000, 1.0850)
exposure_calc.add_position('GBPUSD', 'long', 50000, 1.2650)
exposure_calc.add_position('USDJPY', 'long', 100000, 150.50)
exposure_calc.add_position('EURJPY', 'short', 50000, 163.30)
print("Net Currency Exposure:")
for curr, exp in exposure_calc.get_net_exposure().items():
print(f" {curr}: {exp:,.0f}")
print("\nConcentration Check:")
conc = exposure_calc.check_concentration(30)
print(f" Concentrated: {conc['concentrated']}")
if conc['warnings']:
for w in conc['warnings']:
print(f" Warning: {w}")
print("\nHedge Suggestion for EUR:")
hedge = exposure_calc.suggest_hedge('EUR', 50)
print(f" {hedge}")
class CorrelationRiskManager:
"""Manage risk from correlated positions."""
# Known high correlations between pairs
KNOWN_CORRELATIONS = {
('EURUSD', 'GBPUSD'): 0.85,
('AUDUSD', 'NZDUSD'): 0.90,
('EURUSD', 'USDCHF'): -0.90,
('USDJPY', 'EURJPY'): 0.75,
('AUDUSD', 'USDCAD'): -0.70,
}
def __init__(self):
self.positions: Dict[str, Dict] = {}
self.price_data: Dict[str, pd.Series] = {}
def add_position(self, pair: str, direction: str, risk_amount: float) -> None:
"""Add position with its risk."""
self.positions[pair] = {
'direction': direction,
'risk': risk_amount
}
def add_price_data(self, pair: str, prices: pd.Series) -> None:
"""Add price data for correlation calculation."""
self.price_data[pair] = prices
def get_correlation(self, pair1: str, pair2: str) -> float:
"""Get correlation between two pairs."""
# Check known correlations
key = (pair1, pair2) if (pair1, pair2) in self.KNOWN_CORRELATIONS else (pair2, pair1)
if key in self.KNOWN_CORRELATIONS:
return self.KNOWN_CORRELATIONS[key]
# Calculate from price data
if pair1 in self.price_data and pair2 in self.price_data:
ret1 = self.price_data[pair1].pct_change()
ret2 = self.price_data[pair2].pct_change()
return ret1.corr(ret2)
return 0.0
def calculate_correlated_risk(self) -> Dict:
"""Calculate portfolio risk considering correlations."""
pairs = list(self.positions.keys())
n = len(pairs)
if n == 0:
return {'total_risk': 0, 'correlated_risk': 0}
# Simple risk (sum of individual risks)
simple_risk = sum(pos['risk'] for pos in self.positions.values())
# Correlated risk (using correlation adjustment)
# For same direction positions with high correlation: risk increases
# For opposite direction positions with high correlation: risk decreases (hedge)
correlated_risk_squared = 0
correlation_adjustments = []
for i, pair1 in enumerate(pairs):
pos1 = self.positions[pair1]
risk1 = pos1['risk']
dir1 = 1 if pos1['direction'] == 'long' else -1
correlated_risk_squared += risk1 ** 2
for pair2 in pairs[i+1:]:
pos2 = self.positions[pair2]
risk2 = pos2['risk']
dir2 = 1 if pos2['direction'] == 'long' else -1
corr = self.get_correlation(pair1, pair2)
# Adjust for direction
effective_corr = corr * dir1 * dir2
# Add correlation contribution
correlated_risk_squared += 2 * risk1 * risk2 * effective_corr
if abs(corr) > 0.6:
correlation_adjustments.append({
'pair1': pair1,
'pair2': pair2,
'correlation': corr,
'effective_correlation': effective_corr,
'impact': 'adds_risk' if effective_corr > 0 else 'hedges'
})
correlated_risk = np.sqrt(max(correlated_risk_squared, 0))
return {
'simple_risk': simple_risk,
'correlated_risk': correlated_risk,
'diversification_benefit': simple_risk - correlated_risk,
'correlation_adjustments': correlation_adjustments
}
# Demo
corr_manager = CorrelationRiskManager()
# Add positions (same direction on correlated pairs = more risk)
corr_manager.add_position('EURUSD', 'long', 500)
corr_manager.add_position('GBPUSD', 'long', 500) # High correlation with EUR
corr_manager.add_position('USDCHF', 'long', 300) # Negative correlation (hedge)
print("Correlation Risk Analysis:")
risk = corr_manager.calculate_correlated_risk()
print(f" Simple Risk (sum): ${risk['simple_risk']:.2f}")
print(f" Correlated Risk: ${risk['correlated_risk']:.2f}")
print(f" Diversification Benefit: ${risk['diversification_benefit']:.2f}")
print("\nCorrelation Impacts:")
for adj in risk['correlation_adjustments']:
print(f" {adj['pair1']}/{adj['pair2']}: {adj['correlation']:.2f} -> {adj['impact']}")
9.4 Weekend & Gap Risk
Forex markets close on weekends, creating gap risk that must be managed.
class GapRiskManager:
"""Manage weekend and holiday gap risk."""
# Historical average gap sizes (pips)
TYPICAL_GAP_SIZES = {
'EURUSD': 15,
'GBPUSD': 20,
'USDJPY': 25,
'AUDUSD': 20,
'USDCAD': 15,
}
# Major events that increase gap risk
HIGH_RISK_EVENTS = [
'G7_meeting',
'central_bank_emergency',
'geopolitical_crisis',
'election',
]
def __init__(self):
self.positions: List[Dict] = []
self.upcoming_events: List[str] = []
def add_position(self, pair: str, direction: str,
units: float, current_pnl_pips: float) -> None:
"""Add position for gap risk analysis."""
self.positions.append({
'pair': pair,
'direction': direction,
'units': units,
'current_pnl_pips': current_pnl_pips
})
def set_upcoming_events(self, events: List[str]) -> None:
"""Set upcoming high-impact events."""
self.upcoming_events = events
def calculate_gap_risk(self) -> Dict:
"""Calculate potential gap risk for all positions."""
total_gap_risk = 0
position_risks = []
# Multiplier for high-risk events
event_multiplier = 1.0
if any(e in self.HIGH_RISK_EVENTS for e in self.upcoming_events):
event_multiplier = 2.0
for pos in self.positions:
typical_gap = self.TYPICAL_GAP_SIZES.get(pos['pair'], 20)
worst_case_gap = typical_gap * 3 * event_multiplier
# Calculate potential loss from adverse gap
pip_value = 10 # Assuming standard lot pip value
lots = pos['units'] / 100000
potential_loss = worst_case_gap * pip_value * lots
position_risks.append({
'pair': pos['pair'],
'direction': pos['direction'],
'typical_gap': typical_gap,
'worst_case_gap': worst_case_gap,
'potential_loss': potential_loss,
'current_pnl_pips': pos['current_pnl_pips']
})
total_gap_risk += potential_loss
return {
'total_gap_risk': total_gap_risk,
'event_multiplier': event_multiplier,
'high_risk_events': self.upcoming_events,
'position_risks': position_risks
}
def get_position_reduction_advice(self, account_balance: float,
max_gap_risk_pct: float = 5.0) -> Dict:
"""Advise on position reduction before weekend."""
gap_risk = self.calculate_gap_risk()
current_risk_pct = (gap_risk['total_gap_risk'] / account_balance) * 100
if current_risk_pct <= max_gap_risk_pct:
return {
'reduce_needed': False,
'current_risk_pct': current_risk_pct,
'max_allowed': max_gap_risk_pct
}
# Calculate reduction needed
reduction_factor = max_gap_risk_pct / current_risk_pct
recommendations = []
for pos_risk in gap_risk['position_risks']:
# Prioritize reducing losing positions
if pos_risk['current_pnl_pips'] < 0:
priority = 'high'
suggested_reduction = 0.75 # Close 75%
elif pos_risk['current_pnl_pips'] < 20:
priority = 'medium'
suggested_reduction = 0.50 # Close 50%
else:
priority = 'low'
suggested_reduction = 1 - reduction_factor
recommendations.append({
'pair': pos_risk['pair'],
'priority': priority,
'suggested_reduction': suggested_reduction,
'reason': 'losing' if pos_risk['current_pnl_pips'] < 0 else 'gap_risk'
})
# Sort by priority
recommendations.sort(key=lambda x: {'high': 0, 'medium': 1, 'low': 2}[x['priority']])
return {
'reduce_needed': True,
'current_risk_pct': current_risk_pct,
'max_allowed': max_gap_risk_pct,
'target_reduction': 1 - reduction_factor,
'recommendations': recommendations
}
def should_close_friday(self, position: Dict, account_balance: float,
max_position_gap_risk_pct: float = 2.0) -> Dict:
"""Determine if a position should be closed Friday."""
typical_gap = self.TYPICAL_GAP_SIZES.get(position['pair'], 20)
worst_case = typical_gap * 3
pip_value = 10
lots = position['units'] / 100000
potential_loss = worst_case * pip_value * lots
risk_pct = (potential_loss / account_balance) * 100
# Decision factors
close_reasons = []
if risk_pct > max_position_gap_risk_pct:
close_reasons.append('excessive_gap_risk')
if position.get('current_pnl_pips', 0) < -20:
close_reasons.append('losing_position')
if self.upcoming_events:
close_reasons.append('upcoming_events')
return {
'should_close': len(close_reasons) >= 2,
'risk_pct': risk_pct,
'reasons': close_reasons,
'recommendation': 'close' if len(close_reasons) >= 2 else ('reduce' if close_reasons else 'hold')
}
# Demo
gap_manager = GapRiskManager()
# Add positions
gap_manager.add_position('EURUSD', 'long', 200000, 25) # Winning
gap_manager.add_position('GBPUSD', 'long', 100000, -15) # Losing
gap_manager.add_position('USDJPY', 'short', 150000, 10) # Winning
# Set upcoming events
gap_manager.set_upcoming_events(['G7_meeting'])
print("Gap Risk Analysis:")
risk = gap_manager.calculate_gap_risk()
print(f" Total Gap Risk: ${risk['total_gap_risk']:,.2f}")
print(f" Event Multiplier: {risk['event_multiplier']}x")
print("\nPosition Reduction Advice (50k account):")
advice = gap_manager.get_position_reduction_advice(50000, max_gap_risk_pct=5.0)
print(f" Reduce Needed: {advice['reduce_needed']}")
print(f" Current Risk: {advice['current_risk_pct']:.1f}%")
if advice['reduce_needed']:
print(" Recommendations:")
for rec in advice['recommendations']:
print(f" {rec['pair']}: {rec['priority']} priority, reduce {rec['suggested_reduction']:.0%}")
Exercises
Exercise 1: Forex Position Calculator (Guided)
Complete the ForexPositionCalculator class for comprehensive position sizing.
class ForexPositionCalculator:
"""Complete forex position calculator."""
def __init__(self, account_balance: float, account_currency: str = 'USD'):
self.account_balance = account_balance
self.account_currency = account_currency
self.exchange_rates: Dict[str, float] = {}
def set_rate(self, pair: str, rate: float) -> None:
"""Set exchange rate."""
self.exchange_rates[pair] = rate
def get_pip_value(self, pair: str, lot_size: float = 1.0) -> float:
"""Calculate pip value in account currency."""
pip_size = 0.01 if 'JPY' in pair else ______
units = lot_size * 100000
pip_value = pip_size * units
quote = pair[-3:]
if quote == self.account_currency:
return pip_value
# Convert to account currency
conversion_pair = f"{quote}{self.account_currency}"
if conversion_pair in self.exchange_rates:
return pip_value * self.exchange_rates[conversion_pair]
reverse = f"{self.account_currency}{quote}"
if reverse in self.exchange_rates:
return pip_value / self.exchange_rates[reverse]
return pip_value
def calculate_position(self, pair: str, risk_pct: float,
stop_pips: float) -> Dict:
"""Calculate position size."""
risk_amount = self.account_balance * (risk_pct / ______)
pip_value = self.get_pip_value(pair, 1.0)
lots = risk_amount / (stop_pips * pip_value)
return {
'pair': pair,
'risk_amount': risk_amount,
'lots': round(lots, 2),
'units': int(lots * 100000),
'pip_value': pip_value
}
def validate_position(self, lots: float, pair: str,
max_risk_pct: float = 2.0,
stop_pips: float = 30) -> Dict:
"""Validate if position size is acceptable."""
pip_value = self.get_pip_value(pair, lots)
risk = stop_pips * pip_value
risk_pct = (risk / self.account_balance) * 100
return {
'valid': risk_pct <= ______,
'risk_pct': risk_pct,
'max_allowed': max_risk_pct
}
# Test
calc = ForexPositionCalculator(10000, 'USD')
calc.set_rate('EURUSD', 1.0850)
calc.set_rate('USDJPY', 150.50)
print(f"Position for 1% risk, 25 pip stop: {calc.calculate_position('EURUSD', 1.0, 25)}")
print(f"Validation: {calc.validate_position(0.5, 'EURUSD', 2.0, 30)}")
Solution 1
class ForexPositionCalculator:
def __init__(self, account_balance: float, account_currency: str = 'USD'):
self.account_balance = account_balance
self.account_currency = account_currency
self.exchange_rates: Dict[str, float] = {}
def set_rate(self, pair: str, rate: float) -> None:
self.exchange_rates[pair] = rate
def get_pip_value(self, pair: str, lot_size: float = 1.0) -> float:
pip_size = 0.01 if 'JPY' in pair else 0.0001
units = lot_size * 100000
pip_value = pip_size * units
# ... conversion logic
return pip_value
def calculate_position(self, pair: str, risk_pct: float, stop_pips: float) -> Dict:
risk_amount = self.account_balance * (risk_pct / 100)
pip_value = self.get_pip_value(pair, 1.0)
lots = risk_amount / (stop_pips * pip_value)
return {'pair': pair, 'risk_amount': risk_amount, 'lots': round(lots, 2), ...}
def validate_position(self, lots, pair, max_risk_pct=2.0, stop_pips=30):
# ...
return {'valid': risk_pct <= max_risk_pct, ...}
Exercise 2: Futures Margin Monitor (Guided)
Complete the MarginMonitor class for tracking futures margin.
class MarginMonitor:
"""Monitor futures margin levels."""
CONTRACTS = {
'ES': {'initial': 13200, 'maintenance': 12000},
'NQ': {'initial': 17600, 'maintenance': 16000},
'CL': {'initial': 6500, 'maintenance': 5900},
}
def __init__(self, account_balance: float):
self.account_balance = account_balance
self.positions: Dict[str, int] = {} # symbol -> contracts
self.unrealized_pnl: float = 0
def add_position(self, symbol: str, contracts: int) -> None:
"""Add or update position."""
self.positions[symbol] = self.positions.get(symbol, 0) + contracts
def set_unrealized_pnl(self, pnl: float) -> None:
"""Set current unrealized P&L."""
self.unrealized_pnl = pnl
def get_margin_required(self) -> float:
"""Calculate total margin required."""
total = 0
for symbol, contracts in self.positions.______():
if symbol in self.CONTRACTS:
total += abs(contracts) * self.CONTRACTS[symbol]['initial']
return total
def get_maintenance_margin(self) -> float:
"""Calculate maintenance margin."""
total = 0
for symbol, contracts in self.positions.items():
if symbol in self.CONTRACTS:
total += abs(contracts) * self.CONTRACTS[symbol]['______']
return total
def get_margin_status(self) -> Dict:
"""Get current margin status."""
equity = self.account_balance + self.unrealized_pnl
margin_used = self.get_margin_required()
maintenance = self.get_maintenance_margin()
margin_level = (equity / margin_used * 100) if margin_used > 0 else 100
if margin_level > 150:
status = 'healthy'
elif margin_level > ______:
status = 'warning'
else:
status = 'margin_call'
return {
'equity': equity,
'margin_used': margin_used,
'margin_level': margin_level,
'status': status,
'distance_to_call': equity - maintenance
}
# Test
monitor = MarginMonitor(50000)
monitor.add_position('ES', 2)
monitor.add_position('CL', 1)
monitor.set_unrealized_pnl(-2000)
print(f"Margin Status: {monitor.get_margin_status()}")
Solution 2
class MarginMonitor:
# ... CONTRACTS dict ...
def get_margin_required(self) -> float:
total = 0
for symbol, contracts in self.positions.items():
if symbol in self.CONTRACTS:
total += abs(contracts) * self.CONTRACTS[symbol]['initial']
return total
def get_maintenance_margin(self) -> float:
total = 0
for symbol, contracts in self.positions.items():
if symbol in self.CONTRACTS:
total += abs(contracts) * self.CONTRACTS[symbol]['maintenance']
return total
def get_margin_status(self) -> Dict:
# ...
if margin_level > 150:
status = 'healthy'
elif margin_level > 100:
status = 'warning'
else:
status = 'margin_call'
# ...
Exercise 3: Weekend Risk Advisor (Guided)
Complete the WeekendRiskAdvisor class for Friday position management.
class WeekendRiskAdvisor:
"""Advise on positions before weekend."""
AVG_GAPS = {'EURUSD': 15, 'GBPUSD': 20, 'USDJPY': 25}
def __init__(self, account_balance: float, max_weekend_risk_pct: float = 3.0):
self.account_balance = account_balance
self.max_risk = max_weekend_risk_pct
self.positions: List[Dict] = []
def add_position(self, pair: str, lots: float, pnl_pips: float) -> None:
"""Add position."""
self.positions.append({
'pair': pair,
'lots': lots,
'pnl_pips': pnl_pips
})
def calculate_weekend_risk(self) -> float:
"""Calculate total weekend gap risk."""
total_risk = 0
for pos in self.positions:
gap = self.AVG_GAPS.get(pos['pair'], 20) * 3 # 3x for worst case
risk = gap * pos['lots'] * ______ # pip value
total_risk += risk
return total_risk
def get_advice(self) -> Dict:
"""Get weekend risk advice."""
risk = self.calculate_weekend_risk()
risk_pct = (risk / self.account_balance) * 100
if risk_pct <= self.max_risk:
return {'action': 'hold', 'risk_pct': risk_pct}
# Recommend closing losing positions first
to_close = []
for pos in sorted(self.positions, key=lambda x: x['______']):
if pos['pnl_pips'] < 0:
to_close.append(pos['pair'])
return {
'action': 'reduce',
'risk_pct': risk_pct,
'close_first': to_close
}
# Test
advisor = WeekendRiskAdvisor(20000, 3.0)
advisor.add_position('EURUSD', 0.5, 30)
advisor.add_position('GBPUSD', 0.3, -15)
print(f"Weekend Advice: {advisor.get_advice()}")
Solution 3
class WeekendRiskAdvisor:
# ...
def calculate_weekend_risk(self) -> float:
total_risk = 0
for pos in self.positions:
gap = self.AVG_GAPS.get(pos['pair'], 20) * 3
risk = gap * pos['lots'] * 10 # pip value = $10/lot
total_risk += risk
return total_risk
def get_advice(self) -> Dict:
risk = self.calculate_weekend_risk()
risk_pct = (risk / self.account_balance) * 100
if risk_pct <= self.max_risk:
return {'action': 'hold', 'risk_pct': risk_pct}
# Sort by pnl_pips to close losers first
to_close = []
for pos in sorted(self.positions, key=lambda x: x['pnl_pips']):
if pos['pnl_pips'] < 0:
to_close.append(pos['pair'])
return {'action': 'reduce', 'risk_pct': risk_pct, 'close_first': to_close}
Exercise 4: Complete Risk Dashboard (Open-ended)
Build a comprehensive risk dashboard that: - Tracks all positions (forex and futures) - Calculates total account risk - Monitors margin levels - Provides alerts and recommendations
# Exercise 4: Complete Risk Dashboard (Open-ended)
#
# Requirements:
# 1. Create class RiskDashboard
# 2. Track forex and futures positions
# 3. Calculate total risk as % of account
# 4. Monitor margin utilization
# 5. Generate alerts when risk exceeds limits
# 6. Provide position reduction recommendations
#
# Your implementation:
Solution 4
class RiskDashboard:
def __init__(self, account_balance: float):
self.balance = account_balance
self.forex_positions: List[Dict] = []
self.futures_positions: List[Dict] = []
self.max_risk_pct = 5.0
self.max_margin_pct = 50.0
def add_forex(self, pair, lots, stop_pips, direction):
risk = stop_pips * lots * 10
self.forex_positions.append({
'pair': pair, 'lots': lots, 'risk': risk, 'direction': direction
})
def add_futures(self, symbol, contracts, stop_ticks, tick_value, margin):
risk = stop_ticks * tick_value * contracts
self.futures_positions.append({
'symbol': symbol, 'contracts': contracts, 'risk': risk, 'margin': margin * contracts
})
def total_risk(self):
fx_risk = sum(p['risk'] for p in self.forex_positions)
fut_risk = sum(p['risk'] for p in self.futures_positions)
return fx_risk + fut_risk
def total_margin(self):
return sum(p['margin'] for p in self.futures_positions)
def get_alerts(self):
alerts = []
risk_pct = self.total_risk() / self.balance * 100
margin_pct = self.total_margin() / self.balance * 100
if risk_pct > self.max_risk_pct:
alerts.append(f'RISK: {risk_pct:.1f}% exceeds {self.max_risk_pct}%')
if margin_pct > self.max_margin_pct:
alerts.append(f'MARGIN: {margin_pct:.1f}% exceeds {self.max_margin_pct}%')
return alerts
def get_status(self):
return {
'total_risk': self.total_risk(),
'risk_pct': self.total_risk() / self.balance * 100,
'margin_used': self.total_margin(),
'margin_pct': self.total_margin() / self.balance * 100,
'alerts': self.get_alerts()
}
Exercise 5: Correlation-Adjusted Position Sizer (Open-ended)
Build a position sizer that accounts for correlations between positions.
# Exercise 5: Correlation-Adjusted Position Sizer (Open-ended)
#
# Requirements:
# 1. Create class CorrelationAdjustedSizer
# 2. Store known correlations between pairs
# 3. When adding new position, check correlation with existing
# 4. Reduce size if highly correlated (adds risk)
# 5. Allow larger size if negative correlation (hedges)
#
# Your implementation:
Solution 5
class CorrelationAdjustedSizer:
CORRELATIONS = {
('EURUSD', 'GBPUSD'): 0.85,
('EURUSD', 'USDCHF'): -0.90,
('AUDUSD', 'NZDUSD'): 0.90,
}
def __init__(self, base_risk_pct: float = 1.0):
self.base_risk = base_risk_pct
self.positions: Dict[str, str] = {} # pair -> direction
def get_correlation(self, pair1: str, pair2: str) -> float:
key = (pair1, pair2) if (pair1, pair2) in self.CORRELATIONS else (pair2, pair1)
return self.CORRELATIONS.get(key, 0.0)
def calculate_adjusted_size(self, new_pair: str, new_direction: str) -> float:
max_corr = 0
adds_risk = False
for pair, direction in self.positions.items():
corr = self.get_correlation(new_pair, pair)
effective_corr = corr if direction == new_direction else -corr
if abs(corr) > abs(max_corr):
max_corr = corr
adds_risk = effective_corr > 0
# Adjust risk based on correlation
if adds_risk and abs(max_corr) > 0.7:
return self.base_risk * (1 - abs(max_corr) * 0.5) # Reduce up to 50%
elif not adds_risk and abs(max_corr) > 0.7:
return self.base_risk * (1 + abs(max_corr) * 0.25) # Increase up to 25%
return self.base_risk
def add_position(self, pair: str, direction: str) -> None:
self.positions[pair] = direction
Exercise 6: Dynamic Stop Manager (Open-ended)
Build a stop loss manager that dynamically adjusts stops based on market conditions.
# Exercise 6: Dynamic Stop Manager (Open-ended)
#
# Requirements:
# 1. Create class DynamicStopManager
# 2. Track positions with entry, current stop, direction
# 3. Calculate ATR-based trailing stop
# 4. Implement break-even stop after X pips profit
# 5. Tighten stop as profit increases
# 6. Never move stop against the trade
#
# Your implementation:
Solution 6
class DynamicStopManager:
def __init__(self, breakeven_pips: float = 20, atr_multiplier: float = 2.0):
self.breakeven_pips = breakeven_pips
self.atr_mult = atr_multiplier
self.positions: Dict[str, Dict] = {}
def add_position(self, id: str, entry: float, stop: float, direction: str):
self.positions[id] = {
'entry': entry, 'stop': stop, 'direction': direction, 'highest': entry
}
def update_stop(self, id: str, current_price: float, atr: float) -> Dict:
pos = self.positions.get(id)
if not pos:
return {'error': 'Position not found'}
pip = 0.0001
profit_pips = (current_price - pos['entry']) / pip if pos['direction'] == 'long' else (pos['entry'] - current_price) / pip
# Update highest/lowest
if pos['direction'] == 'long':
pos['highest'] = max(pos['highest'], current_price)
else:
pos['highest'] = min(pos['highest'], current_price)
new_stop = pos['stop']
# Break-even
if profit_pips >= self.breakeven_pips:
if pos['direction'] == 'long':
new_stop = max(new_stop, pos['entry'] + 1 * pip)
else:
new_stop = min(new_stop, pos['entry'] - 1 * pip)
# ATR trailing
if profit_pips >= self.breakeven_pips * 2:
trail_stop = pos['highest'] - (atr * self.atr_mult) if pos['direction'] == 'long' else pos['highest'] + (atr * self.atr_mult)
if pos['direction'] == 'long':
new_stop = max(new_stop, trail_stop)
else:
new_stop = min(new_stop, trail_stop)
pos['stop'] = new_stop
return {'new_stop': new_stop, 'profit_pips': profit_pips}
Module Project: Risk Management System
Build a production-ready risk management system for forex and futures trading.
class RiskManagementSystem:
"""
Production-ready risk management system.
Features: Position sizing, Margin monitoring, Correlation risk,
Gap risk, Dynamic stops, Real-time alerts.
"""
def __init__(self, account_balance: float, account_currency: str = 'USD'):
self.account_balance = account_balance
self.account_currency = account_currency
# Sub-systems
self.forex_calc = ForexRiskCalculator(account_currency)
self.futures_calc = FuturesRiskCalculator()
self.exposure_calc = CurrencyExposureCalculator(account_currency)
self.corr_manager = CorrelationRiskManager()
self.gap_manager = GapRiskManager()
# Risk limits
self.max_single_trade_risk = 2.0 # %
self.max_total_risk = 6.0 # %
self.max_correlation_risk = 4.0 # %
self.max_margin_usage = 50.0 # %
# Active positions
self.forex_positions: List[Dict] = []
self.futures_positions: List[Dict] = []
def set_exchange_rate(self, pair: str, rate: float) -> None:
"""Set exchange rate for calculations."""
self.forex_calc.set_exchange_rate(pair, rate)
def calculate_forex_position(self, pair: str, risk_pct: float,
stop_pips: float) -> Dict:
"""Calculate forex position size."""
if risk_pct > self.max_single_trade_risk:
return {'error': f'Risk {risk_pct}% exceeds max {self.max_single_trade_risk}%'}
return self.forex_calc.calculate_position_size(
self.account_balance, risk_pct, stop_pips, pair
)
def calculate_futures_position(self, symbol: str, risk_pct: float,
stop_ticks: float) -> Dict:
"""Calculate futures position size."""
if risk_pct > self.max_single_trade_risk:
return {'error': f'Risk {risk_pct}% exceeds max {self.max_single_trade_risk}%'}
return self.futures_calc.calculate_position_size(
symbol, self.account_balance, risk_pct, stop_ticks
)
def add_forex_position(self, pair: str, direction: str,
units: float, entry: float, stop_pips: float) -> None:
"""Add active forex position."""
pip_value = self.forex_calc.calculate_pip_value(pair, units / 100000)
risk = stop_pips * pip_value
self.forex_positions.append({
'pair': pair,
'direction': direction,
'units': units,
'entry': entry,
'stop_pips': stop_pips,
'risk': risk
})
# Update sub-systems
self.exposure_calc.add_position(pair, direction, units, entry)
self.corr_manager.add_position(pair, direction, risk)
self.gap_manager.add_position(pair, direction, units, 0)
def add_futures_position(self, symbol: str, direction: str,
contracts: int, stop_ticks: float) -> None:
"""Add active futures position."""
risk_info = self.futures_calc.calculate_tick_risk(symbol, stop_ticks, contracts)
contract = self.futures_calc.get_contract(symbol)
self.futures_positions.append({
'symbol': symbol,
'direction': direction,
'contracts': contracts,
'stop_ticks': stop_ticks,
'risk': risk_info['total_risk'],
'margin': contract.initial_margin * contracts if contract else 0
})
def get_total_risk(self) -> Dict:
"""Calculate total portfolio risk."""
forex_risk = sum(p['risk'] for p in self.forex_positions)
futures_risk = sum(p['risk'] for p in self.futures_positions)
# Get correlated risk
corr_risk = self.corr_manager.calculate_correlated_risk()
simple_total = forex_risk + futures_risk
correlated_total = corr_risk.get('correlated_risk', simple_total)
return {
'forex_risk': forex_risk,
'futures_risk': futures_risk,
'simple_total': simple_total,
'correlated_total': correlated_total,
'risk_pct': (correlated_total / self.account_balance) * 100,
'diversification_benefit': simple_total - correlated_total
}
def get_margin_status(self) -> Dict:
"""Get futures margin status."""
total_margin = sum(p['margin'] for p in self.futures_positions)
margin_pct = (total_margin / self.account_balance) * 100
return {
'margin_used': total_margin,
'margin_pct': margin_pct,
'available': self.account_balance - total_margin,
'within_limits': margin_pct <= self.max_margin_usage
}
def get_alerts(self) -> List[str]:
"""Get risk alerts."""
alerts = []
# Total risk check
risk = self.get_total_risk()
if risk['risk_pct'] > self.max_total_risk:
alerts.append(f"TOTAL RISK: {risk['risk_pct']:.1f}% exceeds {self.max_total_risk}%")
# Margin check
margin = self.get_margin_status()
if not margin['within_limits']:
alerts.append(f"MARGIN: {margin['margin_pct']:.1f}% exceeds {self.max_margin_usage}%")
# Concentration check
conc = self.exposure_calc.check_concentration(30)
if conc['concentrated']:
alerts.extend([f"CONCENTRATION: {w}" for w in conc['warnings']])
return alerts
def can_add_trade(self, risk_amount: float) -> Dict:
"""Check if a new trade can be added within limits."""
current_risk = self.get_total_risk()['correlated_total']
new_total = current_risk + risk_amount
new_pct = (new_total / self.account_balance) * 100
return {
'can_add': new_pct <= self.max_total_risk,
'current_risk_pct': (current_risk / self.account_balance) * 100,
'new_risk_pct': new_pct,
'max_allowed': self.max_total_risk,
'headroom': self.max_total_risk - new_pct
}
def print_dashboard(self) -> None:
"""Print risk dashboard."""
print("\n" + "=" * 60)
print(" RISK MANAGEMENT DASHBOARD")
print(f" Account: ${self.account_balance:,.2f} {self.account_currency}")
print("=" * 60)
# Positions
print("\n" + "-" * 40)
print("POSITIONS")
print("-" * 40)
print(f"Forex: {len(self.forex_positions)} positions")
for p in self.forex_positions:
print(f" {p['pair']} {p['direction']}: {p['units']:,} units, Risk: ${p['risk']:.2f}")
print(f"Futures: {len(self.futures_positions)} positions")
for p in self.futures_positions:
print(f" {p['symbol']} {p['direction']}: {p['contracts']} contracts, Risk: ${p['risk']:.2f}")
# Risk Summary
print("\n" + "-" * 40)
print("RISK SUMMARY")
print("-" * 40)
risk = self.get_total_risk()
print(f"Total Risk: ${risk['correlated_total']:.2f} ({risk['risk_pct']:.1f}%)")
print(f"Diversification Benefit: ${risk['diversification_benefit']:.2f}")
# Margin
print("\n" + "-" * 40)
print("MARGIN STATUS")
print("-" * 40)
margin = self.get_margin_status()
print(f"Margin Used: ${margin['margin_used']:,.2f} ({margin['margin_pct']:.1f}%)")
print(f"Available: ${margin['available']:,.2f}")
# Alerts
alerts = self.get_alerts()
if alerts:
print("\n" + "-" * 40)
print("ALERTS")
print("-" * 40)
for alert in alerts:
print(f" ! {alert}")
else:
print("\n All risk metrics within limits.")
print("\n" + "=" * 60)
# Demo the risk management system
rms = RiskManagementSystem(account_balance=50000, account_currency='USD')
# Set exchange rates
rms.set_exchange_rate('EURUSD', 1.0850)
rms.set_exchange_rate('USDJPY', 150.50)
# Add forex positions
rms.add_forex_position('EURUSD', 'long', 100000, 1.0850, 30)
rms.add_forex_position('GBPUSD', 'long', 50000, 1.2650, 40)
rms.add_forex_position('USDJPY', 'short', 100000, 150.50, 50)
# Add futures position
rms.add_futures_position('ES', 'long', 2, 20)
# Print dashboard
rms.print_dashboard()
# Check if we can add another trade
print("\nCan add $300 risk trade?")
print(rms.can_add_trade(300))
Key Takeaways
- Position Sizing: Always size positions based on risk %, not profit potential; typically 1-2% per trade
- Pip Value: Varies by pair and quote currency; always calculate in account currency
- Futures Risk: Based on tick values and contract specifications; watch margin requirements
- ATR Stops: Dynamic stops that adapt to market volatility; typically 1.5-3x ATR
- Correlation Risk: Same-direction correlated positions multiply risk; opposite directions hedge
- Currency Exposure: Monitor net exposure by currency to avoid concentration
- Weekend Gaps: Reduce position size before weekends; close losing trades first
- Margin Management: Keep margin usage under 50% to handle adverse moves
Next: Module 10 - Backtesting where we'll build robust backtesting systems for forex and futures.
Module 10: Backtesting
Part 3: Risk & Execution
| Duration | Exercises |
|---|---|
| ~2.5 hours | 6 |
Learning Objectives
- Account for realistic forex/futures costs (spreads, swaps, commissions)
- Build a leveraged backtester with margin tracking
- Understand tick data backtesting and variable spreads
- Implement walk-forward optimization for robustness
Prerequisites
- Modules 1-9 (Forex/Futures fundamentals, strategies, risk management)
- Basic backtesting concepts
- Python pandas proficiency
import pandas as pd
import numpy as np
from datetime import datetime, timedelta
from dataclasses import dataclass, field
from typing import List, Dict, Tuple, Optional, Callable
from enum import Enum
import warnings
warnings.filterwarnings('ignore')
10.1 Backtesting Considerations
Forex and futures backtesting requires accounting for spreads, swap/rollover costs, and leverage effects.
Trading Costs Overview
| Cost Type | Forex | Futures | Impact |
|---|---|---|---|
| Spread | 0.5-3 pips | 0.25-1 tick | Per trade |
| Commission | $0-7/lot | $2-5/contract | Per trade |
| Swap/Rollover | Variable | N/A | Daily (forex) |
| Slippage | 0.5-2 pips | 1-2 ticks | Market orders |
@dataclass
class TradingCosts:
"""Trading costs configuration."""
spread_pips: float = 1.0 # Forex spread
commission_per_lot: float = 0.0 # Commission per lot
swap_long: float = 0.0 # Daily swap for long (pips)
swap_short: float = 0.0 # Daily swap for short (pips)
slippage_pips: float = 0.5 # Average slippage
def total_entry_cost_pips(self, is_long: bool) -> float:
"""Total cost to enter a trade in pips."""
return self.spread_pips / 2 + self.slippage_pips
def total_exit_cost_pips(self, is_long: bool) -> float:
"""Total cost to exit a trade in pips."""
return self.spread_pips / 2 + self.slippage_pips
def holding_cost_pips(self, is_long: bool, days: int) -> float:
"""Total swap cost for holding period."""
daily_swap = self.swap_long if is_long else self.swap_short
return daily_swap * days
def commission_pips(self, lots: float, pip_value: float = 10) -> float:
"""Commission converted to pips."""
total_commission = self.commission_per_lot * lots * 2 # Entry + exit
return total_commission / pip_value
class CostCalculator:
"""Calculate realistic trading costs."""
# Typical costs by pair
FOREX_COSTS = {
'EURUSD': TradingCosts(spread_pips=0.8, swap_long=-0.5, swap_short=0.3),
'GBPUSD': TradingCosts(spread_pips=1.2, swap_long=-0.6, swap_short=0.4),
'USDJPY': TradingCosts(spread_pips=1.0, swap_long=0.8, swap_short=-1.2),
'AUDUSD': TradingCosts(spread_pips=1.0, swap_long=0.3, swap_short=-0.5),
'USDCAD': TradingCosts(spread_pips=1.5, swap_long=-0.4, swap_short=0.2),
}
def __init__(self):
self.custom_costs: Dict[str, TradingCosts] = {}
def set_costs(self, symbol: str, costs: TradingCosts) -> None:
"""Set custom costs for a symbol."""
self.custom_costs[symbol] = costs
def get_costs(self, symbol: str) -> TradingCosts:
"""Get costs for a symbol."""
if symbol in self.custom_costs:
return self.custom_costs[symbol]
return self.FOREX_COSTS.get(symbol, TradingCosts())
def calculate_trade_cost(self, symbol: str, lots: float,
is_long: bool, holding_days: int = 0) -> Dict:
"""Calculate total cost for a trade."""
costs = self.get_costs(symbol)
pip_value = 10 * lots # Assuming standard pip value
entry_pips = costs.total_entry_cost_pips(is_long)
exit_pips = costs.total_exit_cost_pips(is_long)
holding_pips = costs.holding_cost_pips(is_long, holding_days)
commission_pips = costs.commission_pips(lots)
total_pips = entry_pips + exit_pips + holding_pips + commission_pips
total_dollars = total_pips * pip_value
return {
'symbol': symbol,
'lots': lots,
'direction': 'long' if is_long else 'short',
'holding_days': holding_days,
'entry_cost_pips': entry_pips,
'exit_cost_pips': exit_pips,
'swap_cost_pips': holding_pips,
'commission_pips': commission_pips,
'total_cost_pips': total_pips,
'total_cost_dollars': total_dollars
}
def break_even_pips(self, symbol: str, lots: float,
is_long: bool, holding_days: int = 0) -> float:
"""Calculate pips needed to break even."""
cost = self.calculate_trade_cost(symbol, lots, is_long, holding_days)
return cost['total_cost_pips']
# Demo
cost_calc = CostCalculator()
print("Trade Cost Calculation:")
cost = cost_calc.calculate_trade_cost('EURUSD', lots=1.0, is_long=True, holding_days=5)
for k, v in cost.items():
if isinstance(v, float):
print(f" {k}: {v:.2f}")
else:
print(f" {k}: {v}")
print(f"\nBreak-even pips: {cost_calc.break_even_pips('EURUSD', 1.0, True, 5):.2f}")
10.2 Building Backtester
A forex-specific backtester must handle leverage, margin, and realistic execution.
@dataclass
class Trade:
"""Represents a single trade."""
id: int
symbol: str
direction: str # 'long' or 'short'
entry_time: datetime
entry_price: float
units: float
stop_loss: float
take_profit: float = None
exit_time: datetime = None
exit_price: float = None
exit_reason: str = None
pnl: float = 0.0
pnl_pips: float = 0.0
class ForexBacktester:
"""Forex-specific backtester with leverage and margin."""
def __init__(self, initial_capital: float, leverage: float = 50,
risk_per_trade: float = 1.0):
self.initial_capital = initial_capital
self.leverage = leverage
self.risk_per_trade = risk_per_trade
self.cost_calculator = CostCalculator()
# State
self.equity = initial_capital
self.balance = initial_capital
self.margin_used = 0.0
self.open_trades: List[Trade] = []
self.closed_trades: List[Trade] = []
self.equity_curve: List[Dict] = []
self.trade_counter = 0
def reset(self) -> None:
"""Reset backtester state."""
self.equity = self.initial_capital
self.balance = self.initial_capital
self.margin_used = 0.0
self.open_trades = []
self.closed_trades = []
self.equity_curve = []
self.trade_counter = 0
def calculate_position_size(self, symbol: str, stop_pips: float) -> float:
"""Calculate position size based on risk."""
risk_amount = self.equity * (self.risk_per_trade / 100)
pip_value_per_unit = 0.0001 if 'JPY' not in symbol else 0.01
# Units = Risk Amount / (Stop Pips * Pip Value)
units = risk_amount / (stop_pips * pip_value_per_unit)
# Check margin constraint
margin_required = units / self.leverage
available_margin = self.equity - self.margin_used
if margin_required > available_margin:
units = available_margin * self.leverage
return int(units)
def open_trade(self, symbol: str, direction: str,
entry_time: datetime, entry_price: float,
stop_pips: float, tp_pips: float = None) -> Optional[Trade]:
"""Open a new trade."""
units = self.calculate_position_size(symbol, stop_pips)
if units <= 0:
return None
pip_size = 0.0001 if 'JPY' not in symbol else 0.01
# Calculate stops
if direction == 'long':
stop_loss = entry_price - (stop_pips * pip_size)
take_profit = entry_price + (tp_pips * pip_size) if tp_pips else None
else:
stop_loss = entry_price + (stop_pips * pip_size)
take_profit = entry_price - (tp_pips * pip_size) if tp_pips else None
# Apply entry costs (slippage)
costs = self.cost_calculator.get_costs(symbol)
slippage = costs.slippage_pips * pip_size
if direction == 'long':
entry_price += slippage
else:
entry_price -= slippage
self.trade_counter += 1
trade = Trade(
id=self.trade_counter,
symbol=symbol,
direction=direction,
entry_time=entry_time,
entry_price=entry_price,
units=units,
stop_loss=stop_loss,
take_profit=take_profit
)
# Update margin
self.margin_used += units / self.leverage
self.open_trades.append(trade)
return trade
def close_trade(self, trade: Trade, exit_time: datetime,
exit_price: float, reason: str) -> None:
"""Close an open trade."""
pip_size = 0.0001 if 'JPY' not in trade.symbol else 0.01
# Apply exit costs
costs = self.cost_calculator.get_costs(trade.symbol)
slippage = costs.slippage_pips * pip_size
if trade.direction == 'long':
exit_price -= slippage
else:
exit_price += slippage
# Calculate P&L
if trade.direction == 'long':
pnl_pips = (exit_price - trade.entry_price) / pip_size
else:
pnl_pips = (trade.entry_price - exit_price) / pip_size
# Subtract spread cost
pnl_pips -= costs.spread_pips
# Calculate holding days and swap
holding_days = (exit_time - trade.entry_time).days
swap_pips = costs.holding_cost_pips(trade.direction == 'long', holding_days)
pnl_pips += swap_pips # Swap can be positive or negative
# Convert to dollars
pnl = pnl_pips * pip_size * trade.units
# Update trade
trade.exit_time = exit_time
trade.exit_price = exit_price
trade.exit_reason = reason
trade.pnl = pnl
trade.pnl_pips = pnl_pips
# Update account
self.balance += pnl
self.equity = self.balance
self.margin_used -= trade.units / self.leverage
# Move to closed
self.open_trades.remove(trade)
self.closed_trades.append(trade)
def update(self, current_time: datetime, current_price: float) -> None:
"""Update open trades and check stops/targets."""
for trade in self.open_trades[:]:
if trade.direction == 'long':
# Check stop loss
if current_price <= trade.stop_loss:
self.close_trade(trade, current_time, trade.stop_loss, 'stop_loss')
# Check take profit
elif trade.take_profit and current_price >= trade.take_profit:
self.close_trade(trade, current_time, trade.take_profit, 'take_profit')
else: # Short
if current_price >= trade.stop_loss:
self.close_trade(trade, current_time, trade.stop_loss, 'stop_loss')
elif trade.take_profit and current_price <= trade.take_profit:
self.close_trade(trade, current_time, trade.take_profit, 'take_profit')
# Update equity curve
self._update_equity(current_time, current_price)
def _update_equity(self, current_time: datetime, current_price: float) -> None:
"""Update equity with unrealized P&L."""
unrealized = 0.0
for trade in self.open_trades:
pip_size = 0.0001 if 'JPY' not in trade.symbol else 0.01
if trade.direction == 'long':
unrealized += (current_price - trade.entry_price) * trade.units
else:
unrealized += (trade.entry_price - current_price) * trade.units
self.equity = self.balance + unrealized
self.equity_curve.append({
'time': current_time,
'equity': self.equity,
'balance': self.balance
})
def run(self, data: pd.DataFrame, strategy: Callable) -> Dict:
"""Run backtest with a strategy function."""
self.reset()
for i in range(len(data)):
row = data.iloc[i]
current_time = row.name if isinstance(row.name, datetime) else data.index[i]
current_price = row['close']
# Update existing trades
self.update(current_time, current_price)
# Check margin call
if self.equity < self.margin_used * 0.5:
# Close all trades
for trade in self.open_trades[:]:
self.close_trade(trade, current_time, current_price, 'margin_call')
continue
# Generate signal from strategy
signal = strategy(data.iloc[:i+1])
if signal and len(self.open_trades) == 0:
self.open_trade(
symbol=signal.get('symbol', 'EURUSD'),
direction=signal['direction'],
entry_time=current_time,
entry_price=current_price,
stop_pips=signal.get('stop_pips', 30),
tp_pips=signal.get('tp_pips')
)
# Close any remaining trades
for trade in self.open_trades[:]:
self.close_trade(trade, data.index[-1], data['close'].iloc[-1], 'end_of_test')
return self.get_results()
def get_results(self) -> Dict:
"""Calculate backtest statistics."""
if not self.closed_trades:
return {'error': 'No trades'}
pnls = [t.pnl for t in self.closed_trades]
pnl_pips = [t.pnl_pips for t in self.closed_trades]
wins = [p for p in pnls if p > 0]
losses = [p for p in pnls if p <= 0]
total_pnl = sum(pnls)
win_rate = len(wins) / len(pnls) * 100 if pnls else 0
avg_win = np.mean(wins) if wins else 0
avg_loss = abs(np.mean(losses)) if losses else 0
profit_factor = sum(wins) / abs(sum(losses)) if losses and sum(losses) != 0 else 0
# Drawdown
equity_values = [e['equity'] for e in self.equity_curve]
peak = equity_values[0]
max_dd = 0
for eq in equity_values:
if eq > peak:
peak = eq
dd = (peak - eq) / peak * 100
max_dd = max(max_dd, dd)
return {
'initial_capital': self.initial_capital,
'final_equity': self.equity,
'total_pnl': total_pnl,
'return_pct': (self.equity / self.initial_capital - 1) * 100,
'total_trades': len(self.closed_trades),
'winning_trades': len(wins),
'losing_trades': len(losses),
'win_rate': win_rate,
'avg_win': avg_win,
'avg_loss': avg_loss,
'profit_factor': profit_factor,
'max_drawdown_pct': max_dd,
'avg_pips': np.mean(pnl_pips),
'total_pips': sum(pnl_pips)
}
# Demo
np.random.seed(42)
dates = pd.date_range('2024-01-01', periods=250, freq='D')
prices = pd.DataFrame({
'open': 1.0800 + np.cumsum(np.random.normal(0.0001, 0.005, 250)),
'high': 0,
'low': 0,
'close': 0
}, index=dates)
prices['close'] = prices['open'] + np.random.normal(0, 0.002, 250)
prices['high'] = prices[['open', 'close']].max(axis=1) + np.random.uniform(0, 0.003, 250)
prices['low'] = prices[['open', 'close']].min(axis=1) - np.random.uniform(0, 0.003, 250)
# Simple MA crossover strategy
def ma_strategy(data: pd.DataFrame) -> Optional[Dict]:
if len(data) < 50:
return None
fast = data['close'].rolling(20).mean().iloc[-1]
slow = data['close'].rolling(50).mean().iloc[-1]
prev_fast = data['close'].rolling(20).mean().iloc[-2]
prev_slow = data['close'].rolling(50).mean().iloc[-2]
# Crossover
if prev_fast <= prev_slow and fast > slow:
return {'direction': 'long', 'stop_pips': 30, 'tp_pips': 60}
elif prev_fast >= prev_slow and fast < slow:
return {'direction': 'short', 'stop_pips': 30, 'tp_pips': 60}
return None
# Run backtest
backtester = ForexBacktester(initial_capital=10000, leverage=50, risk_per_trade=1.0)
results = backtester.run(prices, ma_strategy)
print("Backtest Results:")
for k, v in results.items():
if isinstance(v, float):
print(f" {k}: {v:.2f}")
else:
print(f" {k}: {v}")
10.3 Tick Data Backtesting
Tick data provides the most accurate simulation by capturing variable spreads and real execution conditions.
@dataclass
class Tick:
"""Single tick with bid/ask."""
timestamp: datetime
bid: float
ask: float
@property
def spread(self) -> float:
return self.ask - self.bid
@property
def mid(self) -> float:
return (self.bid + self.ask) / 2
class TickDataBacktester:
"""Backtester using tick-level data with variable spreads."""
def __init__(self, initial_capital: float, leverage: float = 50):
self.initial_capital = initial_capital
self.leverage = leverage
self.equity = initial_capital
self.position = None
self.trades: List[Dict] = []
def reset(self) -> None:
"""Reset state."""
self.equity = self.initial_capital
self.position = None
self.trades = []
def execute_market_order(self, tick: Tick, direction: str,
units: float, stop_pips: float) -> Dict:
"""Execute market order at current tick."""
pip_size = 0.0001
# Use appropriate price (ask for buy, bid for sell)
if direction == 'long':
entry_price = tick.ask # Buy at ask
stop_loss = tick.bid - (stop_pips * pip_size)
else:
entry_price = tick.bid # Sell at bid
stop_loss = tick.ask + (stop_pips * pip_size)
self.position = {
'direction': direction,
'entry_price': entry_price,
'entry_time': tick.timestamp,
'units': units,
'stop_loss': stop_loss,
'entry_spread': tick.spread
}
return self.position
def close_position(self, tick: Tick, reason: str) -> Dict:
"""Close current position."""
if not self.position:
return None
# Use appropriate price
if self.position['direction'] == 'long':
exit_price = tick.bid # Sell at bid
pnl = (exit_price - self.position['entry_price']) * self.position['units']
else:
exit_price = tick.ask # Buy at ask
pnl = (self.position['entry_price'] - exit_price) * self.position['units']
trade = {
**self.position,
'exit_price': exit_price,
'exit_time': tick.timestamp,
'exit_spread': tick.spread,
'pnl': pnl,
'reason': reason
}
self.equity += pnl
self.trades.append(trade)
self.position = None
return trade
def check_stop(self, tick: Tick) -> bool:
"""Check if stop loss is hit."""
if not self.position:
return False
if self.position['direction'] == 'long':
if tick.bid <= self.position['stop_loss']:
self.close_position(tick, 'stop_loss')
return True
else:
if tick.ask >= self.position['stop_loss']:
self.close_position(tick, 'stop_loss')
return True
return False
def analyze_spread_impact(self) -> Dict:
"""Analyze impact of spreads on results."""
if not self.trades:
return {}
spreads = [(t['entry_spread'] + t['exit_spread']) for t in self.trades]
spread_costs = [s * t['units'] for s, t in zip(spreads, self.trades)]
return {
'avg_spread': np.mean(spreads),
'max_spread': max(spreads),
'min_spread': min(spreads),
'total_spread_cost': sum(spread_costs),
'spread_cost_per_trade': np.mean(spread_costs)
}
# Demo with simulated tick data
def generate_tick_data(n_ticks: int, base_price: float = 1.0800) -> List[Tick]:
"""Generate simulated tick data with variable spreads."""
ticks = []
price = base_price
start_time = datetime(2024, 1, 1, 0, 0, 0)
for i in range(n_ticks):
# Random price movement
price += np.random.normal(0, 0.00005)
# Variable spread (wider during volatile times)
base_spread = 0.00008
spread_variation = np.random.uniform(0, 0.00005)
spread = base_spread + spread_variation
tick = Tick(
timestamp=start_time + timedelta(seconds=i),
bid=price - spread/2,
ask=price + spread/2
)
ticks.append(tick)
return ticks
# Generate ticks
np.random.seed(42)
ticks = generate_tick_data(10000)
# Run simple tick-level backtest
tick_bt = TickDataBacktester(initial_capital=10000, leverage=50)
for i, tick in enumerate(ticks):
# Check stops first
tick_bt.check_stop(tick)
# Simple strategy: trade every 1000 ticks
if i % 1000 == 0 and tick_bt.position is None:
direction = 'long' if np.random.random() > 0.5 else 'short'
tick_bt.execute_market_order(tick, direction, units=10000, stop_pips=20)
# Close after 500 ticks in trade
if tick_bt.position and i % 500 == 0:
tick_bt.close_position(tick, 'time_exit')
print("Tick-Level Backtest:")
print(f" Final Equity: ${tick_bt.equity:,.2f}")
print(f" Total Trades: {len(tick_bt.trades)}")
print(f"\nSpread Analysis:")
spread_analysis = tick_bt.analyze_spread_impact()
for k, v in spread_analysis.items():
print(f" {k}: {v:.6f}" if 'spread' in k.lower() else f" {k}: {v:.2f}")
10.4 Walk-Forward Optimization
Walk-forward testing validates strategy robustness by optimizing on in-sample data and testing on out-of-sample data.
class WalkForwardOptimizer:
"""Walk-forward optimization for strategy validation."""
def __init__(self, backtester: ForexBacktester):
self.backtester = backtester
self.results: List[Dict] = []
def create_windows(self, data: pd.DataFrame,
is_period: int, oos_period: int,
step: int = None) -> List[Dict]:
"""Create in-sample and out-of-sample windows."""
if step is None:
step = oos_period
windows = []
start = 0
while start + is_period + oos_period <= len(data):
is_start = start
is_end = start + is_period
oos_start = is_end
oos_end = oos_start + oos_period
windows.append({
'is_data': data.iloc[is_start:is_end],
'oos_data': data.iloc[oos_start:oos_end],
'is_dates': (data.index[is_start], data.index[is_end-1]),
'oos_dates': (data.index[oos_start], data.index[oos_end-1])
})
start += step
return windows
def optimize_parameters(self, data: pd.DataFrame,
param_grid: Dict,
strategy_factory: Callable) -> Dict:
"""Find best parameters on in-sample data."""
best_result = None
best_params = None
best_metric = -np.inf
# Generate all parameter combinations
import itertools
keys = list(param_grid.keys())
values = list(param_grid.values())
for combo in itertools.product(*values):
params = dict(zip(keys, combo))
# Create strategy with these parameters
strategy = strategy_factory(params)
# Run backtest
self.backtester.reset()
result = self.backtester.run(data, strategy)
# Optimize for Sharpe-like metric (return / drawdown)
if result.get('max_drawdown_pct', 100) > 0:
metric = result.get('return_pct', 0) / result.get('max_drawdown_pct', 100)
else:
metric = result.get('return_pct', 0)
if metric > best_metric:
best_metric = metric
best_params = params
best_result = result
return {
'best_params': best_params,
'best_result': best_result,
'optimization_metric': best_metric
}
def run_walk_forward(self, data: pd.DataFrame,
is_period: int, oos_period: int,
param_grid: Dict,
strategy_factory: Callable) -> Dict:
"""Run complete walk-forward analysis."""
windows = self.create_windows(data, is_period, oos_period)
self.results = []
for i, window in enumerate(windows):
print(f"Window {i+1}/{len(windows)}...")
# Optimize on in-sample
opt_result = self.optimize_parameters(
window['is_data'], param_grid, strategy_factory
)
# Test on out-of-sample with best params
best_strategy = strategy_factory(opt_result['best_params'])
self.backtester.reset()
oos_result = self.backtester.run(window['oos_data'], best_strategy)
self.results.append({
'window': i + 1,
'is_dates': window['is_dates'],
'oos_dates': window['oos_dates'],
'best_params': opt_result['best_params'],
'is_result': opt_result['best_result'],
'oos_result': oos_result
})
return self.get_summary()
def get_summary(self) -> Dict:
"""Get walk-forward summary statistics."""
if not self.results:
return {}
is_returns = [r['is_result'].get('return_pct', 0) for r in self.results]
oos_returns = [r['oos_result'].get('return_pct', 0) for r in self.results]
# Walk-forward efficiency
wf_efficiency = np.mean(oos_returns) / np.mean(is_returns) * 100 if np.mean(is_returns) != 0 else 0
return {
'num_windows': len(self.results),
'avg_is_return': np.mean(is_returns),
'avg_oos_return': np.mean(oos_returns),
'wf_efficiency': wf_efficiency,
'oos_win_rate': sum(1 for r in oos_returns if r > 0) / len(oos_returns) * 100,
'total_oos_return': sum(oos_returns),
'is_oos_correlation': np.corrcoef(is_returns, oos_returns)[0, 1] if len(is_returns) > 1 else 0
}
# Demo walk-forward
def create_ma_strategy(params: Dict) -> Callable:
"""Factory to create MA strategy with given parameters."""
fast = params.get('fast', 10)
slow = params.get('slow', 30)
def strategy(data: pd.DataFrame) -> Optional[Dict]:
if len(data) < slow + 5:
return None
fast_ma = data['close'].rolling(fast).mean().iloc[-1]
slow_ma = data['close'].rolling(slow).mean().iloc[-1]
prev_fast = data['close'].rolling(fast).mean().iloc[-2]
prev_slow = data['close'].rolling(slow).mean().iloc[-2]
if prev_fast <= prev_slow and fast_ma > slow_ma:
return {'direction': 'long', 'stop_pips': 25, 'tp_pips': 50}
elif prev_fast >= prev_slow and fast_ma < slow_ma:
return {'direction': 'short', 'stop_pips': 25, 'tp_pips': 50}
return None
return strategy
# Run walk-forward (smaller dataset for demo)
wfo = WalkForwardOptimizer(ForexBacktester(10000, 50, 1.0))
param_grid = {
'fast': [5, 10, 15],
'slow': [20, 30, 40]
}
wf_results = wfo.run_walk_forward(
data=prices,
is_period=100,
oos_period=50,
param_grid=param_grid,
strategy_factory=create_ma_strategy
)
print("\nWalk-Forward Results:")
for k, v in wf_results.items():
print(f" {k}: {v:.2f}" if isinstance(v, float) else f" {k}: {v}")
Exercises
Exercise 1: Cost-Adjusted Backtester (Guided)
Complete the CostAdjustedBacktester that properly accounts for all trading costs.
class CostAdjustedBacktester:
"""Backtester with realistic cost modeling."""
def __init__(self, initial_capital: float):
self.capital = initial_capital
self.equity = initial_capital
self.trades: List[Dict] = []
def calculate_trade_costs(self, entry: float, exit: float,
lots: float, days_held: int,
spread_pips: float = 1.0,
swap_per_day: float = -0.5) -> Dict:
"""Calculate all costs for a trade."""
pip_value = 10 * lots
# Spread cost
spread_cost = spread_pips * ______
# Swap cost
swap_cost = swap_per_day * days_held * pip_value
# Slippage (assume 0.5 pips each way)
slippage_cost = ______ * pip_value * 2
total_cost = spread_cost + swap_cost + slippage_cost
return {
'spread_cost': spread_cost,
'swap_cost': swap_cost,
'slippage_cost': slippage_cost,
'total_cost': total_cost
}
def calculate_net_pnl(self, gross_pnl: float, costs: Dict) -> float:
"""Calculate net P&L after costs."""
return gross_pnl - costs['______']
# Test
bt = CostAdjustedBacktester(10000)
costs = bt.calculate_trade_costs(1.0800, 1.0830, lots=1.0, days_held=3)
print(f"Trade Costs: {costs}")
print(f"Net P&L (from 30 pip gain): ${bt.calculate_net_pnl(300, costs):.2f}")
Solution 1
class CostAdjustedBacktester:
def __init__(self, initial_capital: float):
self.capital = initial_capital
self.equity = initial_capital
self.trades: List[Dict] = []
def calculate_trade_costs(self, entry, exit, lots, days_held,
spread_pips=1.0, swap_per_day=-0.5):
pip_value = 10 * lots
spread_cost = spread_pips * pip_value
swap_cost = swap_per_day * days_held * pip_value
slippage_cost = 0.5 * pip_value * 2
total_cost = spread_cost + swap_cost + slippage_cost
return {..., 'total_cost': total_cost}
def calculate_net_pnl(self, gross_pnl: float, costs: Dict) -> float:
return gross_pnl - costs['total_cost']
Exercise 2: Margin Tracker (Guided)
Complete the MarginTracker class for tracking margin during backtests.
class MarginTracker:
"""Track margin levels during backtest."""
def __init__(self, initial_equity: float, leverage: float = 50):
self.equity = initial_equity
self.leverage = leverage
self.positions: Dict[str, float] = {} # symbol -> units
def get_margin_used(self) -> float:
"""Calculate total margin used."""
total_units = sum(abs(u) for u in self.positions.______())
return total_units / self.leverage
def get_free_margin(self) -> float:
"""Calculate available margin."""
return self.equity - self.______
def get_margin_level(self) -> float:
"""Calculate margin level percentage."""
used = self.get_margin_used()
if used == 0:
return 100.0
return (self.equity / used) * ______
def can_open_position(self, units: float, min_margin_level: float = 100) -> bool:
"""Check if new position is possible."""
new_margin = self.get_margin_used() + (units / self.leverage)
new_level = (self.equity / new_margin) * 100 if new_margin > 0 else 100
return new_level >= min_margin_level
# Test
tracker = MarginTracker(10000, leverage=50)
tracker.positions['EURUSD'] = 100000
print(f"Margin Used: ${tracker.get_margin_used():,.2f}")
print(f"Free Margin: ${tracker.get_free_margin():,.2f}")
print(f"Margin Level: {tracker.get_margin_level():.1f}%")
Solution 2
class MarginTracker:
def __init__(self, initial_equity: float, leverage: float = 50):
self.equity = initial_equity
self.leverage = leverage
self.positions: Dict[str, float] = {}
def get_margin_used(self) -> float:
total_units = sum(abs(u) for u in self.positions.values())
return total_units / self.leverage
def get_free_margin(self) -> float:
return self.equity - self.get_margin_used()
def get_margin_level(self) -> float:
used = self.get_margin_used()
if used == 0:
return 100.0
return (self.equity / used) * 100
Exercise 3: Spread Analyzer (Guided)
Complete the SpreadAnalyzer for analyzing spread patterns in tick data.
class SpreadAnalyzer:
"""Analyze spread patterns from tick data."""
def __init__(self):
self.spreads: List[float] = []
self.timestamps: List[datetime] = []
def add_tick(self, timestamp: datetime, bid: float, ask: float) -> None:
"""Add tick data."""
spread = ask - ______
self.spreads.append(spread)
self.timestamps.append(timestamp)
def get_statistics(self) -> Dict:
"""Get spread statistics."""
if not self.spreads:
return {}
return {
'avg_spread': np.______(self.spreads),
'median_spread': np.median(self.spreads),
'max_spread': max(self.spreads),
'min_spread': min(self.spreads),
'std_spread': np.std(self.spreads)
}
def get_spread_by_hour(self) -> Dict[int, float]:
"""Get average spread by hour."""
hourly = {}
for ts, spread in zip(self.timestamps, self.spreads):
hour = ts.______
if hour not in hourly:
hourly[hour] = []
hourly[hour].append(spread)
return {h: np.mean(s) for h, s in hourly.items()}
# Test
analyzer = SpreadAnalyzer()
for tick in ticks[:1000]:
analyzer.add_tick(tick.timestamp, tick.bid, tick.ask)
print(f"Spread Statistics: {analyzer.get_statistics()}")
Solution 3
class SpreadAnalyzer:
def __init__(self):
self.spreads: List[float] = []
self.timestamps: List[datetime] = []
def add_tick(self, timestamp: datetime, bid: float, ask: float) -> None:
spread = ask - bid
self.spreads.append(spread)
self.timestamps.append(timestamp)
def get_statistics(self) -> Dict:
if not self.spreads:
return {}
return {
'avg_spread': np.mean(self.spreads),
...
}
def get_spread_by_hour(self) -> Dict[int, float]:
hourly = {}
for ts, spread in zip(self.timestamps, self.spreads):
hour = ts.hour
...
Exercise 4: Complete Forex Backtester (Open-ended)
Build a full-featured forex backtester with all realistic components.
# Exercise 4: Complete Forex Backtester (Open-ended)
#
# Requirements:
# 1. Create class FullForexBacktester
# 2. Track: equity, balance, margin, open/closed trades
# 3. Include all costs: spread, swap, slippage, commission
# 4. Handle margin calls (close all if equity < margin)
# 5. Support multiple open positions
# 6. Calculate comprehensive statistics
#
# Your implementation:
Solution 4
class FullForexBacktester:
def __init__(self, capital, leverage=50):
self.initial = capital
self.equity = capital
self.balance = capital
self.leverage = leverage
self.margin_used = 0
self.open_trades = []
self.closed_trades = []
self.costs = CostCalculator()
def open_trade(self, symbol, direction, units, entry, stop, tp=None):
margin = units / self.leverage
if margin > self.equity - self.margin_used:
return None
# Apply entry slippage
costs = self.costs.get_costs(symbol)
slip = costs.slippage_pips * 0.0001
entry = entry + slip if direction == 'long' else entry - slip
trade = {'symbol': symbol, 'direction': direction, 'units': units,
'entry': entry, 'stop': stop, 'tp': tp, 'entry_time': datetime.now()}
self.open_trades.append(trade)
self.margin_used += margin
return trade
def close_trade(self, trade, exit_price, reason):
costs = self.costs.get_costs(trade['symbol'])
# Apply exit costs and calculate PnL
# ...
self.balance += pnl
self.margin_used -= trade['units'] / self.leverage
self.closed_trades.append(trade)
self.open_trades.remove(trade)
def check_margin_call(self):
if self.equity < self.margin_used * 0.5:
for trade in self.open_trades[:]:
self.close_trade(trade, 'margin_call')
def get_statistics(self):
# Calculate win rate, profit factor, max DD, etc.
pass
Exercise 5: Monte Carlo Simulator (Open-ended)
Build a Monte Carlo simulator to test strategy robustness.
# Exercise 5: Monte Carlo Simulator (Open-ended)
#
# Requirements:
# 1. Create class MonteCarloSimulator
# 2. Take backtest trade results as input
# 3. Randomly resample trades to create N simulations
# 4. Calculate distribution of final equity
# 5. Calculate probability of ruin (equity < X%)
# 6. Generate confidence intervals for returns
#
# Your implementation:
Solution 5
class MonteCarloSimulator:
def __init__(self, trade_results: List[float], initial_capital: float):
self.trades = trade_results
self.capital = initial_capital
def run_simulation(self, n_simulations: int = 1000) -> Dict:
final_equities = []
max_drawdowns = []
for _ in range(n_simulations):
# Randomly resample trades
sampled = np.random.choice(self.trades, size=len(self.trades), replace=True)
# Calculate equity curve
equity = self.capital
peak = equity
max_dd = 0
for pnl in sampled:
equity += pnl
if equity > peak:
peak = equity
dd = (peak - equity) / peak
max_dd = max(max_dd, dd)
final_equities.append(equity)
max_drawdowns.append(max_dd)
return {
'mean_equity': np.mean(final_equities),
'median_equity': np.median(final_equities),
'std_equity': np.std(final_equities),
'percentile_5': np.percentile(final_equities, 5),
'percentile_95': np.percentile(final_equities, 95),
'prob_profit': sum(1 for e in final_equities if e > self.capital) / n_simulations,
'prob_ruin': sum(1 for e in final_equities if e < self.capital * 0.5) / n_simulations,
'avg_max_dd': np.mean(max_drawdowns)
}
Exercise 6: Walk-Forward Report Generator (Open-ended)
Build a comprehensive walk-forward analysis report generator.
# Exercise 6: Walk-Forward Report Generator (Open-ended)
#
# Requirements:
# 1. Create class WalkForwardReporter
# 2. Take walk-forward results as input
# 3. Calculate WF efficiency (OOS return / IS return)
# 4. Track parameter stability across windows
# 5. Identify degradation trends
# 6. Generate pass/fail verdict on strategy robustness
#
# Your implementation:
Solution 6
class WalkForwardReporter:
def __init__(self, wf_results: List[Dict]):
self.results = wf_results
def calculate_wf_efficiency(self) -> float:
is_returns = [r['is_result']['return_pct'] for r in self.results]
oos_returns = [r['oos_result']['return_pct'] for r in self.results]
return np.mean(oos_returns) / np.mean(is_returns) * 100 if np.mean(is_returns) != 0 else 0
def analyze_parameter_stability(self) -> Dict:
params_by_window = [r['best_params'] for r in self.results]
# Check how often parameters change
stability = {}
for param in params_by_window[0].keys():
values = [p[param] for p in params_by_window]
stability[param] = {
'unique_values': len(set(values)),
'most_common': max(set(values), key=values.count),
'stability_score': 1 - (len(set(values)) / len(values))
}
return stability
def detect_degradation(self) -> bool:
oos_returns = [r['oos_result']['return_pct'] for r in self.results]
# Check if returns are trending down
if len(oos_returns) < 3:
return False
trend = np.polyfit(range(len(oos_returns)), oos_returns, 1)[0]
return trend < -1 # Significant negative trend
def get_verdict(self) -> Dict:
wf_eff = self.calculate_wf_efficiency()
degrading = self.detect_degradation()
oos_wins = sum(1 for r in self.results if r['oos_result']['return_pct'] > 0)
passed = wf_eff > 50 and not degrading and oos_wins > len(self.results) * 0.5
return {
'verdict': 'PASS' if passed else 'FAIL',
'wf_efficiency': wf_eff,
'degrading': degrading,
'oos_win_rate': oos_wins / len(self.results) * 100,
'reasons': [] if passed else self._get_failure_reasons(wf_eff, degrading, oos_wins)
}
Module Project: Forex/Futures Backtester
Build a production-ready backtesting system with all features.
class ProductionBacktester:
"""
Production-ready forex/futures backtesting system.
Features: Realistic costs, Margin tracking, Walk-forward,
Monte Carlo analysis, Comprehensive reporting.
"""
def __init__(self, initial_capital: float = 10000,
leverage: float = 50, risk_per_trade: float = 1.0):
self.initial_capital = initial_capital
self.leverage = leverage
self.risk_per_trade = risk_per_trade
# Core backtester
self.backtester = ForexBacktester(initial_capital, leverage, risk_per_trade)
self.cost_calc = CostCalculator()
# Results storage
self.backtest_results: Dict = {}
self.wf_results: Dict = {}
self.mc_results: Dict = {}
def run_backtest(self, data: pd.DataFrame, strategy: Callable,
symbol: str = 'EURUSD') -> Dict:
"""Run standard backtest."""
# Set costs for symbol
self.backtester.cost_calculator = self.cost_calc
# Run backtest
self.backtest_results = self.backtester.run(data, strategy)
# Add cost analysis
if self.backtester.closed_trades:
total_spread_cost = sum(
self.cost_calc.get_costs(symbol).spread_pips * 10 * (t.units / 100000)
for t in self.backtester.closed_trades
)
self.backtest_results['total_spread_cost'] = total_spread_cost
return self.backtest_results
def run_walk_forward(self, data: pd.DataFrame,
param_grid: Dict, strategy_factory: Callable,
is_period: int = 100, oos_period: int = 50) -> Dict:
"""Run walk-forward optimization."""
wfo = WalkForwardOptimizer(self.backtester)
self.wf_results = wfo.run_walk_forward(
data, is_period, oos_period, param_grid, strategy_factory
)
return self.wf_results
def run_monte_carlo(self, n_simulations: int = 1000) -> Dict:
"""Run Monte Carlo analysis on backtest results."""
if not self.backtester.closed_trades:
return {'error': 'No trades to analyze'}
pnls = [t.pnl for t in self.backtester.closed_trades]
final_equities = []
max_drawdowns = []
for _ in range(n_simulations):
sampled = np.random.choice(pnls, size=len(pnls), replace=True)
equity = self.initial_capital
peak = equity
max_dd = 0
for pnl in sampled:
equity += pnl
if equity > peak:
peak = equity
dd = (peak - equity) / peak * 100
max_dd = max(max_dd, dd)
final_equities.append(equity)
max_drawdowns.append(max_dd)
self.mc_results = {
'mean_equity': np.mean(final_equities),
'median_equity': np.median(final_equities),
'percentile_5': np.percentile(final_equities, 5),
'percentile_95': np.percentile(final_equities, 95),
'prob_profit': sum(1 for e in final_equities if e > self.initial_capital) / n_simulations * 100,
'prob_ruin_50pct': sum(1 for e in final_equities if e < self.initial_capital * 0.5) / n_simulations * 100,
'avg_max_drawdown': np.mean(max_drawdowns)
}
return self.mc_results
def generate_report(self) -> None:
"""Generate comprehensive backtest report."""
print("\n" + "=" * 70)
print(" BACKTEST REPORT")
print("=" * 70)
# Backtest Results
print("\n" + "-" * 50)
print("BACKTEST RESULTS")
print("-" * 50)
if self.backtest_results:
print(f"Initial Capital: ${self.initial_capital:,.2f}")
print(f"Final Equity: ${self.backtest_results.get('final_equity', 0):,.2f}")
print(f"Total Return: {self.backtest_results.get('return_pct', 0):.2f}%")
print(f"Total Trades: {self.backtest_results.get('total_trades', 0)}")
print(f"Win Rate: {self.backtest_results.get('win_rate', 0):.1f}%")
print(f"Profit Factor: {self.backtest_results.get('profit_factor', 0):.2f}")
print(f"Max Drawdown: {self.backtest_results.get('max_drawdown_pct', 0):.2f}%")
# Walk-Forward Results
if self.wf_results:
print("\n" + "-" * 50)
print("WALK-FORWARD ANALYSIS")
print("-" * 50)
print(f"Windows Tested: {self.wf_results.get('num_windows', 0)}")
print(f"Avg IS Return: {self.wf_results.get('avg_is_return', 0):.2f}%")
print(f"Avg OOS Return: {self.wf_results.get('avg_oos_return', 0):.2f}%")
print(f"WF Efficiency: {self.wf_results.get('wf_efficiency', 0):.1f}%")
print(f"OOS Win Rate: {self.wf_results.get('oos_win_rate', 0):.1f}%")
# Monte Carlo Results
if self.mc_results:
print("\n" + "-" * 50)
print("MONTE CARLO ANALYSIS")
print("-" * 50)
print(f"Mean Final Equity: ${self.mc_results.get('mean_equity', 0):,.2f}")
print(f"5th Percentile: ${self.mc_results.get('percentile_5', 0):,.2f}")
print(f"95th Percentile: ${self.mc_results.get('percentile_95', 0):,.2f}")
print(f"Probability of Profit: {self.mc_results.get('prob_profit', 0):.1f}%")
print(f"Probability of 50% Ruin: {self.mc_results.get('prob_ruin_50pct', 0):.1f}%")
print("\n" + "=" * 70)
# Demo the production backtester
prod_bt = ProductionBacktester(initial_capital=10000, leverage=50, risk_per_trade=1.0)
# Run standard backtest
print("Running backtest...")
results = prod_bt.run_backtest(prices, ma_strategy, 'EURUSD')
# Run walk-forward
print("\nRunning walk-forward analysis...")
wf = prod_bt.run_walk_forward(
prices,
param_grid={'fast': [5, 10], 'slow': [20, 30]},
strategy_factory=create_ma_strategy,
is_period=100,
oos_period=50
)
# Run Monte Carlo
print("\nRunning Monte Carlo analysis...")
mc = prod_bt.run_monte_carlo(n_simulations=500)
# Generate report
prod_bt.generate_report()
Key Takeaways
- Realistic Costs: Include spread, swap, slippage, and commission in all backtests
- Break-Even: Calculate minimum pips needed to cover costs before expecting profit
- Margin Tracking: Monitor margin usage to avoid margin calls; keep under 50%
- Tick Data: Most accurate simulation; captures variable spreads and real execution
- Walk-Forward: Validates strategy robustness; aim for >50% WF efficiency
- Parameter Stability: Stable optimal parameters across windows indicate robustness
- Monte Carlo: Test distribution of outcomes; understand probability of ruin
- Over-Optimization: Beware curve-fitting; out-of-sample results matter most
Next: Module 11 - Live Trading where we'll build systems for real-world execution.
Module 11: Live Trading
| Duration | ~2.5 hours |
| Skill Level | Advanced |
| Prerequisites | Modules 9-10 |
Learning Objectives
By the end of this module, you will be able to: - Evaluate and select appropriate forex/futures brokers - Execute trades programmatically via OANDA API - Integrate with MetaTrader platforms - Design systems for 24-hour continuous operation
Prerequisites
- Completed Modules 9 (Risk Management) and 10 (Backtesting)
- Understanding of order types and position management
- Familiarity with REST APIs
import pandas as pd
import numpy as np
from datetime import datetime, timedelta
from typing import Dict, List, Optional, Tuple
from dataclasses import dataclass, field
from enum import Enum
import time
import logging
Section 11.1: Broker Selection
Choosing the right broker is critical for live trading success.
Forex Broker Considerations
Key Factors: - Regulation: FCA (UK), NFA/CFTC (US), ASIC (Australia) - Spreads: Variable vs fixed, typical vs minimum - Execution: Market maker vs ECN/STP - API Access: REST, FIX, proprietary - Leverage: Varies by jurisdiction (50:1 US, 30:1 EU)
Popular Forex Brokers with API: - OANDA (REST API, well-documented) - Interactive Brokers (comprehensive but complex) - IG Markets (REST API) - Saxo Bank (OpenAPI)
@dataclass
class BrokerProfile:
"""Broker evaluation profile."""
name: str
regulation: List[str]
typical_spread_eurusd: float # in pips
min_deposit: float
max_leverage: int
api_type: str
commission_per_lot: float = 0.0
def total_cost_per_trade(self, lot_size: float = 1.0) -> float:
"""Calculate total cost including spread and commission."""
spread_cost = self.typical_spread_eurusd * 10 * lot_size # $10 per pip per lot
commission = self.commission_per_lot * lot_size * 2 # round trip
return spread_cost + commission
class BrokerComparator:
"""Compare brokers for trading suitability."""
def __init__(self):
self.brokers: List[BrokerProfile] = []
def add_broker(self, broker: BrokerProfile):
"""Add broker to comparison."""
self.brokers.append(broker)
def compare_costs(self, monthly_lots: float = 10.0) -> pd.DataFrame:
"""Compare monthly trading costs."""
data = []
for broker in self.brokers:
cost_per_trade = broker.total_cost_per_trade(1.0)
monthly_cost = cost_per_trade * monthly_lots
data.append({
'Broker': broker.name,
'Spread (pips)': broker.typical_spread_eurusd,
'Commission/lot': broker.commission_per_lot,
'Cost per lot': cost_per_trade,
f'Monthly ({monthly_lots} lots)': monthly_cost
})
return pd.DataFrame(data).sort_values(f'Monthly ({monthly_lots} lots)')
def filter_by_regulation(self, required: List[str]) -> List[BrokerProfile]:
"""Filter brokers by regulation."""
return [
b for b in self.brokers
if any(reg in b.regulation for reg in required)
]
# Example broker comparison
comparator = BrokerComparator()
comparator.add_broker(BrokerProfile(
name="OANDA",
regulation=["FCA", "NFA", "ASIC"],
typical_spread_eurusd=1.0,
min_deposit=0,
max_leverage=50,
api_type="REST"
))
comparator.add_broker(BrokerProfile(
name="Interactive Brokers",
regulation=["SEC", "FCA", "ASIC"],
typical_spread_eurusd=0.1,
min_deposit=0,
max_leverage=50,
api_type="TWS/REST",
commission_per_lot=2.0
))
comparator.add_broker(BrokerProfile(
name="ECN Broker",
regulation=["FCA"],
typical_spread_eurusd=0.2,
min_deposit=1000,
max_leverage=30,
api_type="FIX",
commission_per_lot=3.5
))
print("Broker Cost Comparison (10 lots/month):")
print(comparator.compare_costs(10.0))
Futures Broker Considerations
Futures brokers have different characteristics: - Exchange access: CME, ICE, Eurex - Commission structure: Per-contract fees - Margin rates: Day trading vs overnight - Platform: CQG, Rithmic, TT
@dataclass
class FuturesBrokerProfile:
"""Futures broker profile."""
name: str
exchanges: List[str]
commission_per_contract: float
platform: str
day_margin_es: float # E-mini S&P day margin
overnight_margin_es: float
def annual_cost(self, contracts_per_day: int, trading_days: int = 252) -> float:
"""Calculate annual commission costs."""
return self.commission_per_contract * contracts_per_day * trading_days * 2 # round trip
# Compare futures brokers
futures_brokers = [
FuturesBrokerProfile("AMP Futures", ["CME", "ICE"], 0.59, "Various", 500, 13200),
FuturesBrokerProfile("NinjaTrader", ["CME", "ICE"], 0.53, "NinjaTrader", 500, 13200),
FuturesBrokerProfile("Interactive Brokers", ["CME", "ICE", "Eurex"], 0.85, "TWS", 500, 13200),
]
print("Futures Broker Annual Costs (10 contracts/day):")
for broker in futures_brokers:
annual = broker.annual_cost(10)
print(f" {broker.name}: ${annual:,.0f}")
Section 11.2: OANDA Order Execution
OANDA provides a well-documented REST API for forex trading.
class OrderType(Enum):
MARKET = "MARKET"
LIMIT = "LIMIT"
STOP = "STOP"
MARKET_IF_TOUCHED = "MARKET_IF_TOUCHED"
class OrderSide(Enum):
BUY = "BUY"
SELL = "SELL"
@dataclass
class OrderRequest:
"""Order request structure."""
instrument: str
units: int # positive for buy, negative for sell
order_type: OrderType
price: Optional[float] = None
stop_loss: Optional[float] = None
take_profit: Optional[float] = None
trailing_stop_distance: Optional[float] = None
time_in_force: str = "GTC" # Good Till Cancelled
def to_api_format(self) -> Dict:
"""Convert to OANDA API format."""
order = {
"type": self.order_type.value,
"instrument": self.instrument,
"units": str(self.units),
"timeInForce": self.time_in_force
}
if self.price and self.order_type != OrderType.MARKET:
order["price"] = str(self.price)
if self.stop_loss:
order["stopLossOnFill"] = {"price": str(self.stop_loss)}
if self.take_profit:
order["takeProfitOnFill"] = {"price": str(self.take_profit)}
if self.trailing_stop_distance:
order["trailingStopLossOnFill"] = {
"distance": str(self.trailing_stop_distance)
}
return {"order": order}
@dataclass
class Position:
"""Trading position."""
instrument: str
units: int
average_price: float
unrealized_pnl: float = 0.0
@property
def side(self) -> str:
return "LONG" if self.units > 0 else "SHORT"
class OANDAClient:
"""Simulated OANDA API client for demonstration."""
def __init__(self, account_id: str, practice: bool = True):
self.account_id = account_id
self.practice = practice
self.base_url = "https://api-fxpractice.oanda.com" if practice else "https://api-fxtrade.oanda.com"
# Simulated state
self._balance = 100000.0
self._positions: Dict[str, Position] = {}
self._order_id = 1000
self._prices = {
"EUR_USD": (1.0850, 1.0852), # bid, ask
"GBP_USD": (1.2650, 1.2653),
"USD_JPY": (149.50, 149.53),
}
def get_prices(self, instruments: List[str]) -> Dict[str, Tuple[float, float]]:
"""Get current bid/ask prices."""
return {inst: self._prices.get(inst, (0, 0)) for inst in instruments}
def get_account(self) -> Dict:
"""Get account summary."""
unrealized = sum(p.unrealized_pnl for p in self._positions.values())
return {
"account_id": self.account_id,
"balance": self._balance,
"unrealized_pnl": unrealized,
"nav": self._balance + unrealized,
"margin_used": sum(abs(p.units) * p.average_price / 50 for p in self._positions.values()),
"positions": len(self._positions)
}
def submit_order(self, order: OrderRequest) -> Dict:
"""Submit an order."""
self._order_id += 1
# Get fill price
bid, ask = self._prices.get(order.instrument, (0, 0))
fill_price = ask if order.units > 0 else bid
# Update position
if order.instrument in self._positions:
pos = self._positions[order.instrument]
new_units = pos.units + order.units
if new_units == 0:
del self._positions[order.instrument]
else:
pos.units = new_units
pos.average_price = fill_price
else:
self._positions[order.instrument] = Position(
instrument=order.instrument,
units=order.units,
average_price=fill_price
)
return {
"order_id": str(self._order_id),
"status": "FILLED",
"fill_price": fill_price,
"units": order.units,
"instrument": order.instrument
}
def get_positions(self) -> List[Position]:
"""Get all open positions."""
return list(self._positions.values())
def close_position(self, instrument: str) -> Dict:
"""Close a position."""
if instrument not in self._positions:
return {"error": "No position found"}
pos = self._positions[instrument]
close_order = OrderRequest(
instrument=instrument,
units=-pos.units,
order_type=OrderType.MARKET
)
return self.submit_order(close_order)
# Demonstrate OANDA client usage
client = OANDAClient("101-001-12345678-001", practice=True)
# Check account
print("Account Summary:")
account = client.get_account()
for key, value in account.items():
print(f" {key}: {value}")
# Get prices
print("\nCurrent Prices:")
prices = client.get_prices(["EUR_USD", "GBP_USD"])
for inst, (bid, ask) in prices.items():
print(f" {inst}: {bid}/{ask} (spread: {(ask-bid)*10000:.1f} pips)")
# Submit a market order with stop loss and take profit
order = OrderRequest(
instrument="EUR_USD",
units=10000, # 0.1 lot long
order_type=OrderType.MARKET,
stop_loss=1.0800,
take_profit=1.0950
)
print("Order Request (API format):")
print(order.to_api_format())
print("\nSubmitting order...")
result = client.submit_order(order)
print(f"Result: {result}")
print("\nOpen Positions:")
for pos in client.get_positions():
print(f" {pos.instrument}: {pos.units} @ {pos.average_price} ({pos.side})")
Section 11.3: MetaTrader Integration
MetaTrader 4/5 are popular retail trading platforms with Python integration options.
MetaTrader Overview
MT4 vs MT5: - MT4: Older, forex-focused, MQL4 language - MT5: Newer, multi-asset, MQL5, better Python support
Python Integration Methods: 1. MetaTrader5 package: Official Python library (MT5 only) 2. ZeroMQ bridge: Connect via Expert Advisor 3. File-based: Read/write files for communication 4. REST bridge: Third-party REST API wrappers
class MT5Simulator:
"""Simulated MetaTrader 5 interface."""
def __init__(self):
self._initialized = False
self._account_info = {
'login': 12345678,
'server': 'Demo-Server',
'balance': 100000.0,
'equity': 100000.0,
'margin': 0.0,
'margin_free': 100000.0,
'leverage': 100
}
self._positions = []
self._ticket = 1000000
# Simulated symbols
self._symbols = {
'EURUSD': {'bid': 1.0850, 'ask': 1.0852, 'point': 0.00001, 'digits': 5},
'GBPUSD': {'bid': 1.2650, 'ask': 1.2653, 'point': 0.00001, 'digits': 5},
'USDJPY': {'bid': 149.50, 'ask': 149.53, 'point': 0.001, 'digits': 3},
}
def initialize(self, path: str = None) -> bool:
"""Initialize connection to MT5."""
self._initialized = True
print(f"MT5 initialized (simulated)")
return True
def shutdown(self):
"""Shutdown MT5 connection."""
self._initialized = False
def account_info(self) -> Dict:
"""Get account information."""
if not self._initialized:
return None
return self._account_info
def symbol_info_tick(self, symbol: str) -> Dict:
"""Get current tick for symbol."""
if symbol not in self._symbols:
return None
info = self._symbols[symbol]
return {
'symbol': symbol,
'bid': info['bid'],
'ask': info['ask'],
'time': datetime.now()
}
def order_send(self, request: Dict) -> Dict:
"""Send a trading order."""
if not self._initialized:
return {'retcode': -1, 'comment': 'Not initialized'}
self._ticket += 1
symbol = request.get('symbol')
volume = request.get('volume', 0.1)
order_type = request.get('type', 0) # 0=BUY, 1=SELL
tick = self.symbol_info_tick(symbol)
if not tick:
return {'retcode': -1, 'comment': 'Invalid symbol'}
price = tick['ask'] if order_type == 0 else tick['bid']
return {
'retcode': 10009, # TRADE_RETCODE_DONE
'order': self._ticket,
'volume': volume,
'price': price,
'comment': 'Order executed'
}
def positions_get(self, symbol: str = None) -> List[Dict]:
"""Get open positions."""
return self._positions
class MT5Trader:
"""High-level MT5 trading interface."""
# Order type constants
ORDER_TYPE_BUY = 0
ORDER_TYPE_SELL = 1
ORDER_TYPE_BUY_LIMIT = 2
ORDER_TYPE_SELL_LIMIT = 3
ORDER_TYPE_BUY_STOP = 4
ORDER_TYPE_SELL_STOP = 5
def __init__(self):
self.mt5 = MT5Simulator()
self._connected = False
def connect(self, path: str = None) -> bool:
"""Connect to MetaTrader 5."""
if self.mt5.initialize(path):
self._connected = True
account = self.mt5.account_info()
print(f"Connected to account {account['login']} on {account['server']}")
print(f"Balance: ${account['balance']:,.2f}, Leverage: 1:{account['leverage']}")
return True
return False
def disconnect(self):
"""Disconnect from MT5."""
self.mt5.shutdown()
self._connected = False
def get_price(self, symbol: str) -> Tuple[float, float]:
"""Get bid/ask price."""
tick = self.mt5.symbol_info_tick(symbol)
if tick:
return tick['bid'], tick['ask']
return None, None
def buy(self, symbol: str, volume: float, sl: float = None, tp: float = None) -> Dict:
"""Place a buy order."""
bid, ask = self.get_price(symbol)
if not ask:
return {'error': 'Could not get price'}
request = {
'action': 1, # TRADE_ACTION_DEAL
'symbol': symbol,
'volume': volume,
'type': self.ORDER_TYPE_BUY,
'price': ask,
'deviation': 10,
'magic': 123456,
'comment': 'Python MT5 order',
}
if sl:
request['sl'] = sl
if tp:
request['tp'] = tp
return self.mt5.order_send(request)
def sell(self, symbol: str, volume: float, sl: float = None, tp: float = None) -> Dict:
"""Place a sell order."""
bid, ask = self.get_price(symbol)
if not bid:
return {'error': 'Could not get price'}
request = {
'action': 1,
'symbol': symbol,
'volume': volume,
'type': self.ORDER_TYPE_SELL,
'price': bid,
'deviation': 10,
'magic': 123456,
'comment': 'Python MT5 order',
}
if sl:
request['sl'] = sl
if tp:
request['tp'] = tp
return self.mt5.order_send(request)
# Demonstrate MT5 trading
trader = MT5Trader()
if trader.connect():
# Get price
symbol = "EURUSD"
bid, ask = trader.get_price(symbol)
print(f"\n{symbol}: Bid={bid}, Ask={ask}")
# Place buy order
result = trader.buy(
symbol=symbol,
volume=0.1,
sl=bid - 0.0050, # 50 pip stop
tp=bid + 0.0100 # 100 pip target
)
print(f"\nBuy order result: {result}")
trader.disconnect()
Section 11.4: Continuous Operation
Forex markets trade 24 hours, requiring robust systems for continuous operation.
class TradingSession(Enum):
"""Global trading sessions."""
SYDNEY = "Sydney"
TOKYO = "Tokyo"
LONDON = "London"
NEW_YORK = "New York"
class SessionMonitor:
"""Monitor trading sessions and market hours."""
# Session times in UTC
SESSIONS = {
TradingSession.SYDNEY: (21, 6), # 21:00 - 06:00 UTC
TradingSession.TOKYO: (0, 9), # 00:00 - 09:00 UTC
TradingSession.LONDON: (7, 16), # 07:00 - 16:00 UTC
TradingSession.NEW_YORK: (12, 21), # 12:00 - 21:00 UTC
}
def get_active_sessions(self, utc_hour: int = None) -> List[TradingSession]:
"""Get currently active trading sessions."""
if utc_hour is None:
utc_hour = datetime.utcnow().hour
active = []
for session, (start, end) in self.SESSIONS.items():
if start < end:
if start <= utc_hour < end:
active.append(session)
else: # Crosses midnight
if utc_hour >= start or utc_hour < end:
active.append(session)
return active
def is_weekend(self, dt: datetime = None) -> bool:
"""Check if market is closed for weekend."""
if dt is None:
dt = datetime.utcnow()
# Forex closes Friday 21:00 UTC, opens Sunday 21:00 UTC
if dt.weekday() == 5: # Saturday
return True
if dt.weekday() == 6 and dt.hour < 21: # Sunday before open
return True
if dt.weekday() == 4 and dt.hour >= 21: # Friday after close
return True
return False
def get_session_overlap(self, utc_hour: int = None) -> str:
"""Identify high-liquidity session overlaps."""
active = self.get_active_sessions(utc_hour)
if TradingSession.LONDON in active and TradingSession.NEW_YORK in active:
return "London-NY Overlap (Highest Liquidity)"
elif TradingSession.TOKYO in active and TradingSession.LONDON in active:
return "Tokyo-London Overlap"
elif TradingSession.SYDNEY in active and TradingSession.TOKYO in active:
return "Sydney-Tokyo Overlap"
elif len(active) == 1:
return f"{active[0].value} Session Only"
return "Low Liquidity Period"
# Demonstrate session monitoring
monitor = SessionMonitor()
print("Session Activity by Hour (UTC):")
print("-" * 60)
for hour in range(0, 24, 3):
sessions = monitor.get_active_sessions(hour)
overlap = monitor.get_session_overlap(hour)
session_names = [s.value for s in sessions]
print(f"{hour:02d}:00 - {session_names} - {overlap}")
class SystemHealth:
"""Monitor trading system health."""
def __init__(self):
self.checks: Dict[str, bool] = {}
self.last_check = None
self.errors: List[str] = []
def check_broker_connection(self, client) -> bool:
"""Verify broker connection is active."""
try:
account = client.get_account()
self.checks['broker_connection'] = account is not None
return self.checks['broker_connection']
except Exception as e:
self.errors.append(f"Broker connection error: {e}")
self.checks['broker_connection'] = False
return False
def check_data_feed(self, client, symbols: List[str]) -> bool:
"""Verify price data is flowing."""
try:
prices = client.get_prices(symbols)
valid = all(prices.get(s, (0, 0))[0] > 0 for s in symbols)
self.checks['data_feed'] = valid
return valid
except Exception as e:
self.errors.append(f"Data feed error: {e}")
self.checks['data_feed'] = False
return False
def check_margin_level(self, client, min_level: float = 200.0) -> bool:
"""Check margin level is healthy."""
try:
account = client.get_account()
margin_used = account.get('margin_used', 0)
if margin_used > 0:
margin_level = (account['nav'] / margin_used) * 100
else:
margin_level = float('inf')
self.checks['margin_level'] = margin_level >= min_level
return self.checks['margin_level']
except Exception as e:
self.errors.append(f"Margin check error: {e}")
self.checks['margin_level'] = False
return False
def run_all_checks(self, client, symbols: List[str]) -> Dict[str, bool]:
"""Run all health checks."""
self.errors = []
self.check_broker_connection(client)
self.check_data_feed(client, symbols)
self.check_margin_level(client)
self.last_check = datetime.now()
return self.checks
def is_healthy(self) -> bool:
"""Check if all systems are healthy."""
return all(self.checks.values())
def get_status_report(self) -> str:
"""Generate status report."""
lines = ["System Health Report", "=" * 30]
for check, status in self.checks.items():
icon = "OK" if status else "FAIL"
lines.append(f"{check}: {icon}")
if self.errors:
lines.append("\nErrors:")
for error in self.errors:
lines.append(f" - {error}")
return "\n".join(lines)
# Demonstrate health monitoring
health = SystemHealth()
client = OANDAClient("101-001-12345678-001")
results = health.run_all_checks(client, ["EUR_USD", "GBP_USD"])
print(health.get_status_report())
print(f"\nSystem healthy: {health.is_healthy()}")
class TradingBot:
"""24-hour trading bot framework."""
def __init__(self, client, strategy):
self.client = client
self.strategy = strategy
self.health = SystemHealth()
self.session_monitor = SessionMonitor()
self.running = False
self.trades_today = 0
self.max_daily_trades = 10
# Configure logging
logging.basicConfig(level=logging.INFO)
self.logger = logging.getLogger('TradingBot')
def pre_trade_checks(self) -> bool:
"""Run checks before each trading cycle."""
# Check if weekend
if self.session_monitor.is_weekend():
self.logger.info("Market closed for weekend")
return False
# Check daily trade limit
if self.trades_today >= self.max_daily_trades:
self.logger.warning("Daily trade limit reached")
return False
# Run health checks
self.health.run_all_checks(self.client, ["EUR_USD"])
if not self.health.is_healthy():
self.logger.error("Health check failed")
return False
return True
def run_cycle(self) -> Optional[Dict]:
"""Run one trading cycle."""
if not self.pre_trade_checks():
return None
# Get market data
prices = self.client.get_prices(["EUR_USD"])
# Get signal from strategy
signal = self.strategy.generate_signal(prices)
if signal:
self.logger.info(f"Signal generated: {signal}")
# Execute signal
result = self.execute_signal(signal)
if result and result.get('status') == 'FILLED':
self.trades_today += 1
return result
return None
def execute_signal(self, signal: Dict) -> Dict:
"""Execute a trading signal."""
order = OrderRequest(
instrument=signal['instrument'],
units=signal['units'],
order_type=OrderType.MARKET,
stop_loss=signal.get('stop_loss'),
take_profit=signal.get('take_profit')
)
return self.client.submit_order(order)
def reset_daily_counters(self):
"""Reset counters at start of new day."""
self.trades_today = 0
self.logger.info("Daily counters reset")
# Simple strategy for demonstration
class SimpleStrategy:
"""Simple demonstration strategy."""
def __init__(self, instrument: str = "EUR_USD"):
self.instrument = instrument
self.signal_count = 0
def generate_signal(self, prices: Dict) -> Optional[Dict]:
"""Generate trading signal (simulated)."""
# In real implementation, this would contain actual strategy logic
self.signal_count += 1
# Only generate signal occasionally for demo
if self.signal_count % 5 == 0:
bid, ask = prices.get(self.instrument, (0, 0))
return {
'instrument': self.instrument,
'units': 10000,
'stop_loss': bid - 0.0050,
'take_profit': bid + 0.0100
}
return None
# Demonstrate bot framework
client = OANDAClient("101-001-12345678-001")
strategy = SimpleStrategy("EUR_USD")
bot = TradingBot(client, strategy)
print("Running 3 trading cycles:")
for i in range(3):
print(f"\nCycle {i+1}:")
result = bot.run_cycle()
if result:
print(f" Trade executed: {result}")
else:
print(" No trade")
Exercises
Exercise 1: Broker Comparison (Guided)
Complete the broker scoring system.
class BrokerScorer:
"""Score brokers based on multiple criteria."""
def __init__(self):
# Weights for different criteria
self.weights = {
'cost': 0.30,
'regulation': 0.25,
'api_quality': 0.25,
'leverage': 0.20
}
def score_cost(self, spread_pips: float, commission: float) -> float:
"""Score cost (lower is better). Returns 0-100."""
total_cost = spread_pips * 10 + commission * 2
# Assume $50 is worst case, $5 is best case
score = max(0, 100 - (total_cost - 5) * (100 / 45))
return ______ # Return the score
def score_regulation(self, regulations: List[str]) -> float:
"""Score regulation quality. Returns 0-100."""
tier1 = ['FCA', 'NFA', 'ASIC', 'SEC']
tier2 = ['CySEC', 'FINMA', 'BaFin']
score = 0
for reg in regulations:
if reg in tier1:
score += 40
elif reg in tier2:
score += 25
return ______(score, 100) # Cap at 100
def score_api(self, api_type: str) -> float:
"""Score API quality. Returns 0-100."""
api_scores = {
'REST': 90,
'FIX': 95,
'TWS': 70,
'Proprietary': 50
}
return api_scores.get(______, 30) # Get score or default 30
def calculate_total_score(self, broker: BrokerProfile) -> float:
"""Calculate weighted total score."""
cost_score = self.score_cost(broker.typical_spread_eurusd, broker.commission_per_lot)
reg_score = self.score_regulation(broker.regulation)
api_score = self.score_api(broker.api_type)
leverage_score = min(broker.max_leverage * 2, 100)
total = (
cost_score * self.weights['cost'] +
reg_score * self.weights['regulation'] +
api_score * self.weights['api_quality'] +
leverage_score * self.weights['______'] # Fill in weight key
)
return round(total, 1)
# Test the scorer
scorer = BrokerScorer()
test_broker = BrokerProfile(
name="Test Broker",
regulation=["FCA", "ASIC"],
typical_spread_eurusd=1.0,
min_deposit=0,
max_leverage=50,
api_type="REST"
)
score = scorer.calculate_total_score(test_broker)
print(f"Broker score: {score}/100")
Exercise 2: Order Builder (Guided)
Complete the order builder with validation.
class OrderBuilder:
"""Builder pattern for creating validated orders."""
def __init__(self):
self._instrument: str = None
self._units: int = None
self._order_type: OrderType = OrderType.MARKET
self._price: float = None
self._stop_loss: float = None
self._take_profit: float = None
self._errors: List[str] = []
def instrument(self, instrument: str) -> 'OrderBuilder':
"""Set instrument."""
valid_instruments = ['EUR_USD', 'GBP_USD', 'USD_JPY', 'AUD_USD']
if instrument not in valid_instruments:
self._errors.append(f"Invalid instrument: {instrument}")
self._instrument = ______ # Set the instrument
return self
def units(self, units: int) -> 'OrderBuilder':
"""Set position size (positive=buy, negative=sell)."""
if abs(units) < 1000:
self._errors.append("Minimum order size is 1000 units")
if abs(units) > 10000000:
self._errors.append("Maximum order size is 10M units")
self.______ = units # Set units attribute
return self
def limit_order(self, price: float) -> 'OrderBuilder':
"""Set as limit order."""
self._order_type = OrderType.LIMIT
self._price = price
return self
def stop_loss(self, price: float) -> 'OrderBuilder':
"""Set stop loss."""
self._stop_loss = ______ # Set stop loss price
return self
def take_profit(self, price: float) -> 'OrderBuilder':
"""Set take profit."""
self._take_profit = price
return self
def validate(self) -> bool:
"""Validate order configuration."""
if not self._instrument:
self._errors.append("Instrument is required")
if not self._units:
self._errors.append("Units is required")
# Validate stop loss direction
if self._stop_loss and self._units:
if self._units > 0 and self._stop_loss >= self._price if self._price else False:
self._errors.append("Stop loss must be below entry for long positions")
return len(self._errors) == ______ # Return True if no errors
def build(self) -> OrderRequest:
"""Build and return the order."""
if not self.validate():
raise ValueError(f"Invalid order: {self._errors}")
return OrderRequest(
instrument=self._instrument,
units=self._units,
order_type=self._order_type,
price=self._price,
stop_loss=self._stop_loss,
take_profit=self._take_profit
)
# Test the builder
try:
order = (
OrderBuilder()
.instrument("EUR_USD")
.units(10000)
.stop_loss(1.0800)
.take_profit(1.0950)
.build()
)
print(f"Order created: {order}")
except ValueError as e:
print(f"Error: {e}")
Exercise 3: Connection Manager (Guided)
Complete the connection manager with reconnection logic.
class ConnectionManager:
"""Manage broker connection with auto-reconnect."""
def __init__(self, client, max_retries: int = 3, retry_delay: float = 5.0):
self.client = client
self.max_retries = max_retries
self.retry_delay = retry_delay
self.connected = False
self.connection_attempts = 0
self.last_error: str = None
def connect(self) -> bool:
"""Attempt to connect with retries."""
self.connection_attempts = 0
while self.connection_attempts < self.______: # Check max retries
self.connection_attempts += 1
print(f"Connection attempt {self.connection_attempts}/{self.max_retries}")
try:
# Simulate connection (would call real API)
account = self.client.get_account()
if account:
self.connected = ______ # Set connected status
print("Connected successfully")
return True
except Exception as e:
self.last_error = str(e)
print(f"Connection failed: {e}")
if self.connection_attempts < self.max_retries:
print(f"Retrying in {self.retry_delay} seconds...")
time.sleep(self.______) # Wait before retry
self.connected = False
return False
def ensure_connected(self) -> bool:
"""Ensure connection is active, reconnect if needed."""
if self.connected:
# Verify connection with heartbeat
try:
self.client.get_account()
return True
except:
self.connected = False
return self.connect()
# Test connection manager
client = OANDAClient("test-account")
manager = ConnectionManager(client, max_retries=3, retry_delay=0.1)
if manager.connect():
print("\nConnection established")
print(f"Total attempts: {manager.connection_attempts}")
Exercise 4: Deployment Checklist
Create a comprehensive pre-deployment checklist system for a live trading bot.
# Exercise 4: Build a DeploymentChecklist class that:
# 1. Defines critical checks (broker connection, risk params, strategy validation)
# 2. Defines warning checks (time until weekend, market hours)
# 3. Has a run_all() method that executes all checks
# 4. Returns a report showing pass/fail status and any warnings
# Your code here
Exercise 5: Alert System
Build an alert system for monitoring live trades.
# Exercise 5: Build an AlertSystem class that:
# 1. Monitors positions for stop loss proximity (within X pips)
# 2. Monitors daily P&L against thresholds (max loss, target profit)
# 3. Monitors margin level
# 4. Generates alerts with severity levels (INFO, WARNING, CRITICAL)
# 5. Has a method to get all active alerts
# Your code here
Exercise 6: Error Recovery
Implement error handling and recovery for common trading issues.
# Exercise 6: Build an ErrorRecovery class that:
# 1. Handles order rejection (retry with adjusted parameters)
# 2. Handles connection loss (attempt reconnection)
# 3. Handles position reconciliation (compare local vs broker positions)
# 4. Logs all errors and recovery attempts
# 5. Implements circuit breaker pattern (disable trading after N errors)
# Your code here
Module Project: Live Trading System
Build a complete live trading system framework.
class LiveTradingSystem:
"""
Complete live trading system.
Integrates:
- Broker connection with auto-reconnect
- Session monitoring
- Health checks
- Order management
- Alerting
- Logging
"""
def __init__(self, config: Dict):
"""
Initialize trading system.
Config should include:
- broker: Broker client instance
- strategy: Trading strategy instance
- risk_params: Risk management parameters
- instruments: List of instruments to trade
"""
self.config = config
self.client = config['broker']
self.strategy = config['strategy']
self.instruments = config.get('instruments', ['EUR_USD'])
# Components
self.session_monitor = SessionMonitor()
self.health = SystemHealth()
# State
self.running = False
self.paused = False
self.daily_pnl = 0.0
self.trades_today = 0
# Risk parameters
self.max_daily_loss = config.get('risk_params', {}).get('max_daily_loss', -1000)
self.max_daily_trades = config.get('risk_params', {}).get('max_daily_trades', 10)
# Setup logging
self._setup_logging()
def _setup_logging(self):
"""Configure logging."""
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
self.logger = logging.getLogger('LiveTradingSystem')
def startup_checks(self) -> bool:
"""Run all startup checks."""
self.logger.info("Running startup checks...")
# Check broker connection
if not self.health.check_broker_connection(self.client):
self.logger.error("Broker connection failed")
return False
# Check data feed
if not self.health.check_data_feed(self.client, self.instruments):
self.logger.error("Data feed check failed")
return False
# Check margin
if not self.health.check_margin_level(self.client):
self.logger.error("Margin level check failed")
return False
self.logger.info("All startup checks passed")
return True
def can_trade(self) -> Tuple[bool, str]:
"""Check if trading is allowed."""
# Check if paused
if self.paused:
return False, "System is paused"
# Check weekend
if self.session_monitor.is_weekend():
return False, "Market closed for weekend"
# Check daily loss limit
if self.daily_pnl <= self.max_daily_loss:
return False, f"Daily loss limit reached: {self.daily_pnl}"
# Check daily trade limit
if self.trades_today >= self.max_daily_trades:
return False, "Daily trade limit reached"
return True, "OK"
def process_signal(self, signal: Dict) -> Optional[Dict]:
"""Process and execute a trading signal."""
can_trade, reason = self.can_trade()
if not can_trade:
self.logger.warning(f"Cannot trade: {reason}")
return None
# Create order
order = OrderRequest(
instrument=signal['instrument'],
units=signal['units'],
order_type=OrderType.MARKET,
stop_loss=signal.get('stop_loss'),
take_profit=signal.get('take_profit')
)
# Execute
self.logger.info(f"Executing order: {signal['instrument']} {signal['units']} units")
result = self.client.submit_order(order)
if result.get('status') == 'FILLED':
self.trades_today += 1
self.logger.info(f"Order filled: {result}")
else:
self.logger.warning(f"Order not filled: {result}")
return result
def run_cycle(self):
"""Run one trading cycle."""
# Health check
if not self.health.is_healthy():
self.health.run_all_checks(self.client, self.instruments)
if not self.health.is_healthy():
self.logger.warning("Health check failed, skipping cycle")
return
# Get prices
prices = self.client.get_prices(self.instruments)
# Generate signal
signal = self.strategy.generate_signal(prices)
if signal:
self.process_signal(signal)
def pause(self):
"""Pause trading."""
self.paused = True
self.logger.info("Trading paused")
def resume(self):
"""Resume trading."""
self.paused = False
self.logger.info("Trading resumed")
def shutdown(self):
"""Graceful shutdown."""
self.logger.info("Initiating shutdown...")
self.running = False
# Close all positions
positions = self.client.get_positions()
for pos in positions:
self.logger.info(f"Closing position: {pos.instrument}")
self.client.close_position(pos.instrument)
self.logger.info("Shutdown complete")
def get_status(self) -> Dict:
"""Get current system status."""
account = self.client.get_account()
sessions = self.session_monitor.get_active_sessions()
return {
'running': self.running,
'paused': self.paused,
'healthy': self.health.is_healthy(),
'daily_pnl': self.daily_pnl,
'trades_today': self.trades_today,
'active_sessions': [s.value for s in sessions],
'balance': account.get('balance', 0),
'positions': account.get('positions', 0)
}
# Demonstrate the live trading system
config = {
'broker': OANDAClient("101-001-12345678-001"),
'strategy': SimpleStrategy("EUR_USD"),
'instruments': ['EUR_USD', 'GBP_USD'],
'risk_params': {
'max_daily_loss': -500,
'max_daily_trades': 5
}
}
system = LiveTradingSystem(config)
# Run startup checks
if system.startup_checks():
print("\nSystem Status:")
status = system.get_status()
for key, value in status.items():
print(f" {key}: {value}")
# Run a few cycles
print("\nRunning trading cycles:")
for i in range(3):
system.run_cycle()
# Check final status
print(f"\nTrades executed: {system.trades_today}")
Key Takeaways
-
Broker Selection: Evaluate brokers on regulation, costs, API quality, and execution
-
Order Execution: Use proper order types with stop loss and take profit for risk management
-
Platform Integration: OANDA REST API and MetaTrader 5 offer different trade-offs for Python integration
-
24-Hour Operation: Forex requires session awareness, health monitoring, and automatic recovery
-
System Design: Production systems need connection management, health checks, alerting, and graceful shutdown
-
Risk Controls: Always implement daily loss limits, trade limits, and margin monitoring
Next Module: Module 12 - Advanced Strategies (Currency Indices, Spread Trading)
Module 12: Advanced Strategies
| Duration | ~2.5 hours |
| Skill Level | Advanced |
| Prerequisites | Modules 8-11 |
Learning Objectives
By the end of this module, you will be able to: - Build and trade custom currency indices - Implement spread and pairs trading strategies - Understand options on forex and futures - Identify and trade seasonal patterns
Prerequisites
- Completed Part 3 (Risk & Execution)
- Understanding of technical and fundamental analysis
- Familiarity with backtesting concepts
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from datetime import datetime, timedelta
from typing import Dict, List, Optional, Tuple
from dataclasses import dataclass, field
from enum import Enum
from scipy import stats
Section 12.1: Currency Indices
Currency indices measure a currency's strength against a basket of currencies.
The US Dollar Index (DXY)
The DXY measures USD against 6 major currencies: - EUR (57.6%) - JPY (13.6%) - GBP (11.9%) - CAD (9.1%) - SEK (4.2%) - CHF (3.6%)
Formula: DXY = 50.14348112 × (EUR/USD)^-0.576 × (USD/JPY)^0.136 × ...
class DollarIndex:
"""Calculate the US Dollar Index (DXY)."""
# Official DXY weights
WEIGHTS = {
'EUR': -0.576, # Negative because EUR/USD
'JPY': 0.136, # Positive because USD/JPY
'GBP': -0.119, # Negative because GBP/USD
'CAD': 0.091, # Positive because USD/CAD
'SEK': 0.042, # Positive because USD/SEK
'CHF': 0.036 # Positive because USD/CHF
}
CONSTANT = 50.14348112
def calculate(self, rates: Dict[str, float]) -> float:
"""
Calculate DXY from exchange rates.
rates should contain:
- EURUSD, USDJPY, GBPUSD, USDCAD, USDSEK, USDCHF
"""
dxy = self.CONSTANT
# Convert rates to proper format
rate_map = {
'EUR': rates.get('EURUSD', 1.0),
'JPY': rates.get('USDJPY', 100.0),
'GBP': rates.get('GBPUSD', 1.0),
'CAD': rates.get('USDCAD', 1.0),
'SEK': rates.get('USDSEK', 10.0),
'CHF': rates.get('USDCHF', 1.0)
}
for currency, weight in self.WEIGHTS.items():
rate = rate_map[currency]
dxy *= rate ** weight
return dxy
def calculate_contribution(self, rates: Dict[str, float],
prev_rates: Dict[str, float]) -> Dict[str, float]:
"""Calculate each currency's contribution to DXY change."""
contributions = {}
rate_map = {
'EUR': ('EURUSD', -1),
'JPY': ('USDJPY', 1),
'GBP': ('GBPUSD', -1),
'CAD': ('USDCAD', 1),
'SEK': ('USDSEK', 1),
'CHF': ('USDCHF', 1)
}
for currency, (pair, direction) in rate_map.items():
current = rates.get(pair, 1.0)
previous = prev_rates.get(pair, 1.0)
change = (current / previous - 1) * direction
weight = abs(self.WEIGHTS[currency])
contributions[currency] = change * weight * 100 # in percentage points
return contributions
# Calculate DXY example
dxy = DollarIndex()
current_rates = {
'EURUSD': 1.0850,
'USDJPY': 149.50,
'GBPUSD': 1.2650,
'USDCAD': 1.3500,
'USDSEK': 10.50,
'USDCHF': 0.8800
}
index_value = dxy.calculate(current_rates)
print(f"DXY Value: {index_value:.2f}")
# Calculate contribution from rate changes
previous_rates = {
'EURUSD': 1.0900,
'USDJPY': 148.00,
'GBPUSD': 1.2700,
'USDCAD': 1.3450,
'USDSEK': 10.40,
'USDCHF': 0.8750
}
contributions = dxy.calculate_contribution(current_rates, previous_rates)
print("\nContributions to DXY change:")
for currency, contrib in sorted(contributions.items(), key=lambda x: abs(x[1]), reverse=True):
print(f" {currency}: {contrib:+.3f}%")
class CustomCurrencyIndex:
"""Build custom currency strength indices."""
def __init__(self, base_currency: str, pairs: List[str], weights: Dict[str, float] = None):
"""
Initialize custom index.
base_currency: Currency to measure strength of (e.g., 'EUR')
pairs: List of pairs involving this currency
weights: Optional custom weights (default: equal)
"""
self.base_currency = base_currency
self.pairs = pairs
if weights:
self.weights = weights
else:
# Equal weights
self.weights = {pair: 1.0 / len(pairs) for pair in pairs}
def calculate(self, rates: Dict[str, float], base_value: float = 100.0) -> float:
"""Calculate index value from rates."""
index = 0.0
for pair, weight in self.weights.items():
rate = rates.get(pair, 1.0)
# Determine if base currency is first or second in pair
if pair.startswith(self.base_currency):
# EUR in EURUSD - higher rate = stronger EUR
contribution = rate * weight
else:
# EUR in GBPEUR - lower rate = stronger EUR
contribution = (1 / rate) * weight
index += contribution
return index * base_value
def calculate_strength(self, rates_series: pd.DataFrame) -> pd.Series:
"""Calculate index over time series."""
values = []
for _, row in rates_series.iterrows():
rates = row.to_dict()
values.append(self.calculate(rates))
return pd.Series(values, index=rates_series.index)
# Create EUR strength index
eur_index = CustomCurrencyIndex(
base_currency='EUR',
pairs=['EURUSD', 'EURJPY', 'EURGBP', 'EURCHF'],
weights={
'EURUSD': 0.40, # Most important
'EURJPY': 0.25,
'EURGBP': 0.20,
'EURCHF': 0.15
}
)
sample_rates = {
'EURUSD': 1.0850,
'EURJPY': 162.20,
'EURGBP': 0.8580,
'EURCHF': 0.9550
}
eur_strength = eur_index.calculate(sample_rates)
print(f"EUR Strength Index: {eur_strength:.2f}")
Section 12.2: Spread Trading
Spread trading involves simultaneously buying and selling related instruments.
@dataclass
class SpreadTrade:
"""Represents a spread trade."""
long_instrument: str
short_instrument: str
long_units: float
short_units: float
entry_spread: float
current_spread: float = 0.0
@property
def pnl(self) -> float:
"""Calculate P&L from spread change."""
return self.current_spread - self.entry_spread
class InterCommoditySpread:
"""Trade spreads between related commodities."""
# Common commodity spreads
COMMON_SPREADS = {
'crack_spread': ('CL', 'RB'), # Crude oil vs Gasoline
'crush_spread': ('ZS', 'ZM'), # Soybeans vs Soybean Meal
'gold_silver': ('GC', 'SI'), # Gold vs Silver ratio
'brent_wti': ('BZ', 'CL'), # Brent vs WTI crude
}
def __init__(self, spread_type: str):
if spread_type not in self.COMMON_SPREADS:
raise ValueError(f"Unknown spread type: {spread_type}")
self.spread_type = spread_type
self.long_sym, self.short_sym = self.COMMON_SPREADS[spread_type]
def calculate_spread(self, long_price: float, short_price: float) -> float:
"""Calculate spread value."""
if self.spread_type == 'gold_silver':
# Gold/Silver ratio
return long_price / short_price
else:
# Price difference
return long_price - short_price
def analyze_spread(self, spread_history: pd.Series) -> Dict:
"""Analyze spread statistics."""
return {
'current': spread_history.iloc[-1],
'mean': spread_history.mean(),
'std': spread_history.std(),
'z_score': (spread_history.iloc[-1] - spread_history.mean()) / spread_history.std(),
'percentile': stats.percentileofscore(spread_history, spread_history.iloc[-1]),
'min': spread_history.min(),
'max': spread_history.max()
}
# Generate synthetic gold/silver ratio data
np.random.seed(42)
dates = pd.date_range('2024-01-01', periods=252, freq='D')
# Gold/Silver ratio typically ranges 60-90
ratio_mean = 75
ratio_std = 5
ratios = ratio_mean + ratio_std * np.cumsum(np.random.randn(252) * 0.1)
spread_series = pd.Series(ratios, index=dates)
# Analyze the spread
gs_spread = InterCommoditySpread('gold_silver')
analysis = gs_spread.analyze_spread(spread_series)
print("Gold/Silver Ratio Analysis:")
for key, value in analysis.items():
print(f" {key}: {value:.2f}")
# Plot
plt.figure(figsize=(12, 6))
plt.plot(spread_series, label='Gold/Silver Ratio')
plt.axhline(y=analysis['mean'], color='r', linestyle='--', label=f"Mean: {analysis['mean']:.1f}")
plt.axhline(y=analysis['mean'] + 2*analysis['std'], color='g', linestyle=':', label='+2 Std')
plt.axhline(y=analysis['mean'] - 2*analysis['std'], color='g', linestyle=':', label='-2 Std')
plt.title('Gold/Silver Ratio with Bollinger Bands')
plt.xlabel('Date')
plt.ylabel('Ratio')
plt.legend()
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
class CalendarSpread:
"""Trade spreads between different contract months."""
def __init__(self, symbol: str, front_month: str, back_month: str):
self.symbol = symbol
self.front_month = front_month
self.back_month = back_month
def calculate_spread(self, front_price: float, back_price: float) -> float:
"""Calculate calendar spread (back - front)."""
return back_price - front_price
def determine_market_structure(self, front_price: float, back_price: float) -> str:
"""Determine contango or backwardation."""
spread = self.calculate_spread(front_price, back_price)
if spread > 0:
return "Contango" # Back month more expensive
elif spread < 0:
return "Backwardation" # Front month more expensive
else:
return "Flat"
def calculate_roll_yield(self, front_price: float, back_price: float,
days_to_expiry: int) -> float:
"""Calculate annualized roll yield."""
if front_price == 0 or days_to_expiry == 0:
return 0.0
spread_pct = (back_price - front_price) / front_price
annualized = spread_pct * (365 / days_to_expiry)
return annualized * 100 # As percentage
# Example: Crude Oil calendar spread
oil_spread = CalendarSpread('CL', 'Mar24', 'Jun24')
front_price = 72.50
back_price = 73.80
spread = oil_spread.calculate_spread(front_price, back_price)
structure = oil_spread.determine_market_structure(front_price, back_price)
roll_yield = oil_spread.calculate_roll_yield(front_price, back_price, 90)
print(f"Crude Oil Calendar Spread:")
print(f" Front ({oil_spread.front_month}): ${front_price}")
print(f" Back ({oil_spread.back_month}): ${back_price}")
print(f" Spread: ${spread:.2f}")
print(f" Market Structure: {structure}")
print(f" Annualized Roll Yield: {roll_yield:.2f}%")
class CurrencyPairsTrader:
"""Statistical arbitrage on correlated currency pairs."""
def __init__(self, pair1: str, pair2: str, lookback: int = 60):
self.pair1 = pair1
self.pair2 = pair2
self.lookback = lookback
self.hedge_ratio = 1.0
def calculate_hedge_ratio(self, prices1: pd.Series, prices2: pd.Series) -> float:
"""Calculate optimal hedge ratio using linear regression."""
# Use recent prices for regression
p1 = prices1.iloc[-self.lookback:]
p2 = prices2.iloc[-self.lookback:]
slope, intercept, r_value, p_value, std_err = stats.linregress(p2, p1)
self.hedge_ratio = slope
self.correlation = r_value
return slope
def calculate_spread(self, price1: float, price2: float) -> float:
"""Calculate spread using hedge ratio."""
return price1 - self.hedge_ratio * price2
def calculate_spread_series(self, prices1: pd.Series, prices2: pd.Series) -> pd.Series:
"""Calculate spread time series."""
return prices1 - self.hedge_ratio * prices2
def generate_signals(self, spread: pd.Series, z_threshold: float = 2.0) -> pd.Series:
"""Generate trading signals based on z-score."""
mean = spread.rolling(self.lookback).mean()
std = spread.rolling(self.lookback).std()
z_score = (spread - mean) / std
signals = pd.Series(0, index=spread.index)
signals[z_score > z_threshold] = -1 # Short spread
signals[z_score < -z_threshold] = 1 # Long spread
return signals
# Generate synthetic correlated pair data
np.random.seed(42)
n = 252
dates = pd.date_range('2024-01-01', periods=n, freq='D')
# EURUSD and GBPUSD are highly correlated
common_factor = np.cumsum(np.random.randn(n) * 0.001)
eurusd = 1.08 + common_factor + np.cumsum(np.random.randn(n) * 0.0005)
gbpusd = 1.26 + common_factor * 1.1 + np.cumsum(np.random.randn(n) * 0.0006)
eurusd_series = pd.Series(eurusd, index=dates)
gbpusd_series = pd.Series(gbpusd, index=dates)
# Create pairs trader
pairs_trader = CurrencyPairsTrader('EURUSD', 'GBPUSD', lookback=30)
hedge_ratio = pairs_trader.calculate_hedge_ratio(eurusd_series, gbpusd_series)
print(f"Hedge Ratio: {hedge_ratio:.4f}")
print(f"Correlation: {pairs_trader.correlation:.4f}")
# Calculate spread and signals
spread = pairs_trader.calculate_spread_series(eurusd_series, gbpusd_series)
signals = pairs_trader.generate_signals(spread)
print(f"\nSignal distribution:")
print(signals.value_counts())
Section 12.3: Options on Futures
Options provide additional strategic flexibility in forex and futures trading.
class OptionType(Enum):
CALL = "call"
PUT = "put"
@dataclass
class FXOption:
"""FX Option representation."""
pair: str
option_type: OptionType
strike: float
expiry_days: int
premium: float
notional: float = 100000 # Standard lot
def intrinsic_value(self, spot: float) -> float:
"""Calculate intrinsic value."""
if self.option_type == OptionType.CALL:
return max(0, spot - self.strike) * self.notional
else:
return max(0, self.strike - spot) * self.notional
def payoff_at_expiry(self, spot: float) -> float:
"""Calculate P&L at expiry."""
return self.intrinsic_value(spot) - self.premium
def breakeven(self) -> float:
"""Calculate breakeven price."""
premium_per_unit = self.premium / self.notional
if self.option_type == OptionType.CALL:
return self.strike + premium_per_unit
else:
return self.strike - premium_per_unit
class BlackScholes:
"""Black-Scholes option pricing for FX options."""
@staticmethod
def d1(S: float, K: float, r_d: float, r_f: float, sigma: float, T: float) -> float:
"""Calculate d1."""
return (np.log(S / K) + (r_d - r_f + 0.5 * sigma ** 2) * T) / (sigma * np.sqrt(T))
@staticmethod
def d2(d1: float, sigma: float, T: float) -> float:
"""Calculate d2."""
return d1 - sigma * np.sqrt(T)
@classmethod
def price(cls, S: float, K: float, r_d: float, r_f: float,
sigma: float, T: float, option_type: OptionType) -> float:
"""
Calculate option price using Garman-Kohlhagen (FX Black-Scholes).
S: Spot rate
K: Strike
r_d: Domestic interest rate
r_f: Foreign interest rate
sigma: Volatility
T: Time to expiry in years
"""
if T <= 0:
return 0.0
d1 = cls.d1(S, K, r_d, r_f, sigma, T)
d2 = cls.d2(d1, sigma, T)
if option_type == OptionType.CALL:
price = S * np.exp(-r_f * T) * stats.norm.cdf(d1) - K * np.exp(-r_d * T) * stats.norm.cdf(d2)
else:
price = K * np.exp(-r_d * T) * stats.norm.cdf(-d2) - S * np.exp(-r_f * T) * stats.norm.cdf(-d1)
return price
@classmethod
def delta(cls, S: float, K: float, r_d: float, r_f: float,
sigma: float, T: float, option_type: OptionType) -> float:
"""Calculate option delta."""
if T <= 0:
return 0.0
d1 = cls.d1(S, K, r_d, r_f, sigma, T)
if option_type == OptionType.CALL:
return np.exp(-r_f * T) * stats.norm.cdf(d1)
else:
return np.exp(-r_f * T) * (stats.norm.cdf(d1) - 1)
# Price an EURUSD call option
spot = 1.0850
strike = 1.0900
r_usd = 0.05 # USD rate
r_eur = 0.04 # EUR rate
volatility = 0.08 # 8% annualized vol
days_to_expiry = 30
T = days_to_expiry / 365
call_price = BlackScholes.price(spot, strike, r_usd, r_eur, volatility, T, OptionType.CALL)
put_price = BlackScholes.price(spot, strike, r_usd, r_eur, volatility, T, OptionType.PUT)
call_delta = BlackScholes.delta(spot, strike, r_usd, r_eur, volatility, T, OptionType.CALL)
put_delta = BlackScholes.delta(spot, strike, r_usd, r_eur, volatility, T, OptionType.PUT)
print(f"EURUSD Option Pricing:")
print(f" Spot: {spot}")
print(f" Strike: {strike}")
print(f" Days to Expiry: {days_to_expiry}")
print(f" Volatility: {volatility*100}%")
print(f"\nCall Option:")
print(f" Price: {call_price:.5f} ({call_price*100000:.2f} USD per 100k lot)")
print(f" Delta: {call_delta:.4f}")
print(f"\nPut Option:")
print(f" Price: {put_price:.5f} ({put_price*100000:.2f} USD per 100k lot)")
print(f" Delta: {put_delta:.4f}")
class DeltaHedger:
"""Delta hedging for option positions."""
def __init__(self, option: FXOption, hedge_frequency: str = 'daily'):
self.option = option
self.hedge_frequency = hedge_frequency
self.spot_position = 0.0
self.hedge_history = []
def calculate_hedge(self, spot: float, r_d: float, r_f: float,
sigma: float, days_remaining: int) -> float:
"""Calculate required spot hedge."""
T = days_remaining / 365
delta = BlackScholes.delta(
spot, self.option.strike, r_d, r_f, sigma, T, self.option.option_type
)
# For a long option, we need to sell delta * notional of spot
required_hedge = -delta * self.option.notional
return required_hedge
def rebalance(self, spot: float, r_d: float, r_f: float,
sigma: float, days_remaining: int) -> Dict:
"""Rebalance the hedge."""
required = self.calculate_hedge(spot, r_d, r_f, sigma, days_remaining)
adjustment = required - self.spot_position
self.spot_position = required
trade = {
'spot': spot,
'required_hedge': required,
'adjustment': adjustment,
'days_remaining': days_remaining
}
self.hedge_history.append(trade)
return trade
# Example delta hedging
option = FXOption(
pair='EURUSD',
option_type=OptionType.CALL,
strike=1.0900,
expiry_days=30,
premium=500,
notional=100000
)
hedger = DeltaHedger(option)
# Simulate hedging over a few days
spots = [1.0850, 1.0880, 1.0920, 1.0900]
days_left = [30, 29, 28, 27]
print("Delta Hedging Simulation:")
print("-" * 60)
for spot, days in zip(spots, days_left):
trade = hedger.rebalance(spot, 0.05, 0.04, 0.08, days)
print(f"Day {30-days+1}: Spot={spot:.4f}, Hedge={trade['required_hedge']:.0f}, "
f"Adjustment={trade['adjustment']:+.0f}")
Section 12.4: Seasonal Strategies
Many markets exhibit predictable seasonal patterns.
class SeasonalAnalyzer:
"""Analyze seasonal patterns in price data."""
def __init__(self, prices: pd.Series):
self.prices = prices
self.returns = prices.pct_change().dropna()
def monthly_returns(self) -> pd.DataFrame:
"""Calculate average returns by month."""
monthly = self.returns.groupby(self.returns.index.month).agg(['mean', 'std', 'count'])
monthly.columns = ['avg_return', 'std', 'observations']
monthly['t_stat'] = monthly['avg_return'] / (monthly['std'] / np.sqrt(monthly['observations']))
monthly['win_rate'] = self.returns.groupby(self.returns.index.month).apply(lambda x: (x > 0).mean())
return monthly
def day_of_week_returns(self) -> pd.DataFrame:
"""Calculate average returns by day of week."""
daily = self.returns.groupby(self.returns.index.dayofweek).agg(['mean', 'std', 'count'])
daily.columns = ['avg_return', 'std', 'observations']
daily.index = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri']
return daily
def turn_of_month(self, window: int = 3) -> Dict:
"""Analyze turn-of-month effect."""
# Last N days of month
month_end = self.returns[self.returns.index.is_month_end |
(self.returns.index + pd.Timedelta(days=1)).is_month_start]
# First N days of month
month_start = self.returns[self.returns.index.is_month_start |
(self.returns.index.day <= window)]
return {
'month_end_avg': month_end.mean(),
'month_start_avg': month_start.mean(),
'rest_avg': self.returns[~self.returns.index.isin(month_end.index.union(month_start.index))].mean()
}
# Generate multi-year synthetic data with seasonal patterns
np.random.seed(42)
dates = pd.date_range('2020-01-01', '2024-12-31', freq='D')
# Base random walk
returns = np.random.randn(len(dates)) * 0.01
# Add monthly seasonality (e.g., January effect)
month_effects = {
1: 0.002, # January positive
5: -0.001, # May negative (sell in May)
9: -0.001, # September negative
12: 0.002 # December positive (Santa rally)
}
for i, date in enumerate(dates):
if date.month in month_effects:
returns[i] += month_effects[date.month]
prices = 100 * np.exp(np.cumsum(returns))
price_series = pd.Series(prices, index=dates)
# Analyze seasonality
analyzer = SeasonalAnalyzer(price_series)
monthly = analyzer.monthly_returns()
print("Monthly Return Analysis:")
print(monthly[['avg_return', 'win_rate', 't_stat']].round(4))
# Plot monthly returns
plt.figure(figsize=(10, 5))
months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
colors = ['green' if x > 0 else 'red' for x in monthly['avg_return']]
plt.bar(months, monthly['avg_return'] * 100, color=colors, alpha=0.7)
plt.title('Average Monthly Returns (%)')
plt.ylabel('Return (%)')
plt.axhline(y=0, color='black', linestyle='-', linewidth=0.5)
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
class SeasonalStrategy:
"""Trade based on seasonal patterns."""
def __init__(self, long_months: List[int], short_months: List[int] = None):
"""
Initialize strategy.
long_months: Months to go long (1-12)
short_months: Months to go short (optional)
"""
self.long_months = long_months
self.short_months = short_months or []
def generate_signals(self, dates: pd.DatetimeIndex) -> pd.Series:
"""Generate position signals."""
signals = pd.Series(0, index=dates)
for date in dates:
if date.month in self.long_months:
signals[date] = 1
elif date.month in self.short_months:
signals[date] = -1
return signals
def backtest(self, prices: pd.Series) -> pd.DataFrame:
"""Backtest the seasonal strategy."""
returns = prices.pct_change().dropna()
signals = self.generate_signals(returns.index)
strategy_returns = returns * signals.shift(1) # Signal from previous day
results = pd.DataFrame({
'returns': returns,
'signal': signals,
'strategy_returns': strategy_returns
})
results['cumulative'] = (1 + results['strategy_returns'].fillna(0)).cumprod()
results['buy_hold'] = (1 + results['returns'].fillna(0)).cumprod()
return results
# Test "Sell in May" strategy
sell_in_may = SeasonalStrategy(
long_months=[11, 12, 1, 2, 3, 4], # Long Nov-Apr
short_months=[] # Stay flat May-Oct
)
results = sell_in_may.backtest(price_series)
print("Sell in May Strategy Results:")
print(f" Strategy Total Return: {(results['cumulative'].iloc[-1] - 1) * 100:.2f}%")
print(f" Buy & Hold Return: {(results['buy_hold'].iloc[-1] - 1) * 100:.2f}%")
print(f" Time in Market: {(results['signal'] != 0).mean() * 100:.1f}%")
Exercises
Exercise 1: GBP Index (Guided)
Create a GBP strength index.
class GBPStrengthIndex:
"""Calculate GBP strength against major currencies."""
def __init__(self):
# Define pairs and weights
self.pairs = ['GBPUSD', 'GBPEUR', 'GBPJPY', 'GBPCHF', 'GBPAUD']
self.weights = {
'GBPUSD': 0.30, # USD most important
'GBPEUR': 0.25, # EUR second
'GBPJPY': ______, # Fill in weight
'GBPCHF': 0.10,
'GBPAUD': 0.15
}
def calculate(self, rates: Dict[str, float]) -> float:
"""Calculate GBP index value."""
index_value = 0.0
for pair, weight in self.weights.items():
rate = rates.get(pair, 1.0)
# GBP is always first in these pairs
# Higher rate = stronger GBP
index_value += rate * ______ # Apply weight
return index_value * 100 # Scale to 100
def rate_of_change(self, current: Dict[str, float],
previous: Dict[str, float]) -> float:
"""Calculate index change."""
current_idx = self.______(current) # Call calculate method
previous_idx = self.calculate(previous)
return (current_idx / previous_idx - 1) * ______ # Return as percentage
# Test the index
gbp_index = GBPStrengthIndex()
rates = {
'GBPUSD': 1.2650,
'GBPEUR': 1.1650,
'GBPJPY': 189.00,
'GBPCHF': 1.1100,
'GBPAUD': 1.9200
}
value = gbp_index.calculate(rates)
print(f"GBP Strength Index: {value:.2f}")
Exercise 2: Spread Mean Reversion (Guided)
Build a mean reversion strategy for spreads.
class SpreadMeanReversion:
"""Mean reversion strategy for spread trading."""
def __init__(self, lookback: int = 20, entry_z: float = 2.0, exit_z: float = 0.5):
self.lookback = lookback
self.entry_z = entry_z
self.exit_z = exit_z
self.position = 0 # -1, 0, or 1
def calculate_z_score(self, spread: pd.Series) -> float:
"""Calculate current z-score."""
recent = spread.iloc[-self.lookback:]
mean = recent.______() # Calculate mean
std = recent.std()
current = spread.iloc[-1]
if std == 0:
return 0.0
return (current - mean) / ______ # Calculate z-score
def generate_signal(self, spread: pd.Series) -> int:
"""Generate trading signal."""
z = self.calculate_z_score(spread)
if self.position == 0: # No position
if z > self.entry_z:
self.position = ______ # Short the spread
elif z < -self.entry_z:
self.position = 1 # Long the spread
else: # Have position
if abs(z) < self.______: # Check exit threshold
self.position = 0 # Exit
return self.position
# Test the strategy
strategy = SpreadMeanReversion(lookback=20, entry_z=2.0, exit_z=0.5)
# Use spread_series from earlier
signals = []
for i in range(20, len(spread_series)):
signal = strategy.generate_signal(spread_series.iloc[:i+1])
signals.append(signal)
print(f"Signal distribution: {pd.Series(signals).value_counts().to_dict()}")
Exercise 3: Option Payoff Calculator (Guided)
Create a payoff diagram generator.
class OptionPayoffCalculator:
"""Calculate and visualize option payoffs."""
def __init__(self):
self.positions: List[Dict] = []
def add_call(self, strike: float, premium: float, quantity: int = 1):
"""Add a call option position."""
self.positions.append({
'type': 'call',
'strike': strike,
'premium': premium,
'quantity': quantity
})
def add_put(self, strike: float, premium: float, quantity: int = 1):
"""Add a put option position."""
self.positions.append({
'type': ______, # Set type
'strike': strike,
'premium': premium,
'quantity': quantity
})
def calculate_payoff(self, spot: float) -> float:
"""Calculate total payoff at given spot price."""
total = 0.0
for pos in self.positions:
if pos['type'] == 'call':
intrinsic = max(0, spot - pos['strike'])
else: # put
intrinsic = max(0, pos['______'] - spot) # Get strike
payoff = (intrinsic - pos['premium']) * pos['quantity']
total += payoff
return total
def plot_payoff(self, spot_range: Tuple[float, float], title: str = "Option Payoff"):
"""Plot payoff diagram."""
spots = np.linspace(spot_range[0], spot_range[1], 100)
payoffs = [self.calculate_payoff(s) for s in spots]
plt.figure(figsize=(10, 6))
plt.plot(spots, payoffs, 'b-', linewidth=2)
plt.axhline(y=0, color='black', linestyle='-', linewidth=0.5)
plt.fill_between(spots, payoffs, 0, where=[p > 0 for p in payoffs],
alpha=0.3, color='green', label='Profit')
plt.fill_between(spots, payoffs, 0, where=[p < 0 for p in payoffs],
alpha=0.3, color='red', label='Loss')
plt.xlabel('Spot Price')
plt.ylabel('P&L')
plt.title(title)
plt.legend()
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.______() # Display the plot
# Create a bull call spread
calc = OptionPayoffCalculator()
calc.add_call(strike=1.08, premium=0.02, quantity=1) # Buy lower strike
calc.add_call(strike=1.10, premium=0.01, quantity=-1) # Sell higher strike
calc.plot_payoff((1.04, 1.14), "Bull Call Spread - EURUSD")
Exercise 4: Multi-Currency Basket
Create a customizable multi-currency basket index.
# Exercise 4: Build a CurrencyBasket class that:
# 1. Allows adding multiple currency pairs with custom weights
# 2. Normalizes weights to sum to 1.0
# 3. Calculates basket value from current rates
# 4. Tracks basket value over time series
# 5. Calculates correlation between individual pairs and basket
# Your code here
Exercise 5: Calendar Spread Analyzer
Build a comprehensive calendar spread analysis tool.
# Exercise 5: Build a CalendarSpreadAnalyzer class that:
# 1. Takes prices for multiple contract months
# 2. Calculates all pairwise spreads
# 3. Identifies the term structure (contango/backwardation)
# 4. Finds the most attractive spread based on historical z-score
# 5. Generates a term structure plot
# Your code here
Exercise 6: Seasonal Strategy Optimizer
Optimize a seasonal trading strategy.
# Exercise 6: Build a SeasonalOptimizer class that:
# 1. Tests all combinations of months to be long/short/flat
# 2. Evaluates strategies using Sharpe ratio
# 3. Applies out-of-sample validation
# 4. Returns the optimal month selection
# 5. Reports statistical significance of patterns
# Your code here
Module Project: Advanced Strategy Toolkit
Build a comprehensive toolkit for advanced forex/futures strategies.
class AdvancedStrategyToolkit:
"""
Comprehensive toolkit for advanced trading strategies.
Integrates:
- Currency indices
- Spread trading
- Options analysis
- Seasonal patterns
"""
def __init__(self):
self.dxy = DollarIndex()
self.seasonal = None
self.spreads = {}
def analyze_currency_strength(self, rates: Dict[str, float]) -> Dict[str, float]:
"""
Calculate strength for all major currencies.
Returns normalized strength scores.
"""
# Calculate DXY
dxy_value = self.dxy.calculate(rates)
# Calculate other currency indices
eur_index = CustomCurrencyIndex('EUR', ['EURUSD', 'EURJPY', 'EURGBP'])
gbp_index = CustomCurrencyIndex('GBP', ['GBPUSD', 'GBPJPY', 'GBPCHF'])
jpy_pairs = ['USDJPY', 'EURJPY', 'GBPJPY']
# Normalize (100 = average)
strengths = {
'USD': dxy_value,
'EUR': eur_index.calculate(rates),
'GBP': gbp_index.calculate(rates)
}
return strengths
def find_spread_opportunities(self, prices: Dict[str, pd.Series],
z_threshold: float = 2.0) -> List[Dict]:
"""
Scan for spread trading opportunities.
Returns list of opportunities sorted by z-score.
"""
opportunities = []
# Define related pairs to check
related_pairs = [
('EURUSD', 'GBPUSD'),
('AUDUSD', 'NZDUSD'),
('USDJPY', 'EURJPY'),
]
for pair1, pair2 in related_pairs:
if pair1 in prices and pair2 in prices:
trader = CurrencyPairsTrader(pair1, pair2)
trader.calculate_hedge_ratio(prices[pair1], prices[pair2])
spread = trader.calculate_spread_series(prices[pair1], prices[pair2])
# Calculate z-score
mean = spread.rolling(60).mean().iloc[-1]
std = spread.rolling(60).std().iloc[-1]
z_score = (spread.iloc[-1] - mean) / std if std > 0 else 0
if abs(z_score) > z_threshold:
opportunities.append({
'pair1': pair1,
'pair2': pair2,
'z_score': z_score,
'hedge_ratio': trader.hedge_ratio,
'correlation': trader.correlation,
'signal': 'SHORT_SPREAD' if z_score > 0 else 'LONG_SPREAD'
})
return sorted(opportunities, key=lambda x: abs(x['z_score']), reverse=True)
def analyze_seasonality(self, prices: pd.Series) -> Dict:
"""
Comprehensive seasonal analysis.
"""
self.seasonal = SeasonalAnalyzer(prices)
monthly = self.seasonal.monthly_returns()
dow = self.seasonal.day_of_week_returns()
tom = self.seasonal.turn_of_month()
# Find significant patterns
significant_months = monthly[abs(monthly['t_stat']) > 1.96].index.tolist()
return {
'monthly': monthly,
'day_of_week': dow,
'turn_of_month': tom,
'significant_months': significant_months,
'best_month': monthly['avg_return'].idxmax(),
'worst_month': monthly['avg_return'].idxmin()
}
def price_option_strategy(self, spot: float, strategies: List[Dict],
r_d: float, r_f: float, sigma: float, T: float) -> Dict:
"""
Price a multi-leg option strategy.
strategies: List of {'type': 'call'/'put', 'strike': K, 'quantity': N}
"""
total_premium = 0
total_delta = 0
legs = []
for strat in strategies:
opt_type = OptionType.CALL if strat['type'] == 'call' else OptionType.PUT
price = BlackScholes.price(spot, strat['strike'], r_d, r_f, sigma, T, opt_type)
delta = BlackScholes.delta(spot, strat['strike'], r_d, r_f, sigma, T, opt_type)
legs.append({
'type': strat['type'],
'strike': strat['strike'],
'quantity': strat['quantity'],
'price': price,
'delta': delta
})
total_premium += price * strat['quantity']
total_delta += delta * strat['quantity']
return {
'legs': legs,
'net_premium': total_premium,
'net_delta': total_delta
}
def generate_report(self, rates: Dict, prices: Dict[str, pd.Series]) -> str:
"""Generate comprehensive market analysis report."""
lines = ["=" * 60]
lines.append("ADVANCED STRATEGY TOOLKIT REPORT")
lines.append(f"Generated: {datetime.now().strftime('%Y-%m-%d %H:%M')}")
lines.append("=" * 60)
# Currency strength
lines.append("\n--- CURRENCY STRENGTH ---")
strengths = self.analyze_currency_strength(rates)
for ccy, strength in strengths.items():
lines.append(f"{ccy}: {strength:.2f}")
# Spread opportunities
lines.append("\n--- SPREAD OPPORTUNITIES ---")
opps = self.find_spread_opportunities(prices, z_threshold=1.5)
if opps:
for opp in opps[:3]:
lines.append(f"{opp['pair1']}/{opp['pair2']}: Z={opp['z_score']:.2f} -> {opp['signal']}")
else:
lines.append("No significant opportunities found")
lines.append("\n" + "=" * 60)
return "\n".join(lines)
# Demonstrate the toolkit
toolkit = AdvancedStrategyToolkit()
# Sample rates
rates = {
'EURUSD': 1.0850,
'USDJPY': 149.50,
'GBPUSD': 1.2650,
'USDCAD': 1.3500,
'USDSEK': 10.50,
'USDCHF': 0.8800,
'EURJPY': 162.20,
'EURGBP': 0.8580,
'GBPJPY': 189.00,
'GBPCHF': 1.1100
}
# Generate synthetic price series for spread analysis
np.random.seed(42)
n = 100
dates = pd.date_range('2024-01-01', periods=n, freq='D')
prices = {
'EURUSD': pd.Series(1.08 + np.cumsum(np.random.randn(n) * 0.002), index=dates),
'GBPUSD': pd.Series(1.26 + np.cumsum(np.random.randn(n) * 0.002), index=dates),
}
# Generate report
report = toolkit.generate_report(rates, prices)
print(report)
# Analyze seasonality
seasonal = toolkit.analyze_seasonality(price_series)
print(f"\nBest Month: {seasonal['best_month']}")
print(f"Worst Month: {seasonal['worst_month']}")
Key Takeaways
-
Currency Indices: DXY and custom indices measure relative currency strength
-
Spread Trading: Inter-commodity, calendar, and pairs spreads offer relative value opportunities
-
Options: FX options add strategic flexibility; delta hedging manages directional risk
-
Seasonality: Calendar effects exist but require statistical validation
-
Integration: Combining multiple approaches improves trading decisions
Next Module: Module 13 - Automation & Monitoring
Module 13: Automation & Monitoring
| Duration | ~2.5 hours |
| Skill Level | Advanced |
| Prerequisites | Modules 11-12 |
Learning Objectives
By the end of this module, you will be able to: - Build 24/7 scheduling systems with timezone awareness - Implement comprehensive alert and notification systems - Track real-time performance metrics - Monitor system health and implement failover logic
Prerequisites
- Completed Module 11 (Live Trading)
- Understanding of trading sessions and market hours
- Familiarity with logging and error handling
import pandas as pd
import numpy as np
from datetime import datetime, timedelta, time
from typing import Dict, List, Optional, Callable, Any
from dataclasses import dataclass, field
from enum import Enum
import logging
import json
from collections import deque
import threading
import time as time_module
Section 13.1: Scheduling for 24/7
Forex markets trade 24 hours, requiring sophisticated scheduling with timezone awareness.
class Timezone(Enum):
"""Major financial timezones."""
UTC = 0
NEW_YORK = -5 # EST (adjust for DST)
LONDON = 0 # GMT (adjust for DST)
TOKYO = 9
SYDNEY = 11
FRANKFURT = 1
class TimezoneConverter:
"""Handle timezone conversions for global markets."""
# DST transitions (simplified)
DST_REGIONS = {
'US': {'start': (3, 2), 'end': (11, 1)}, # Mar 2nd Sun, Nov 1st Sun
'EU': {'start': (3, 4), 'end': (10, 4)}, # Mar last Sun, Oct last Sun
'AU': {'start': (10, 1), 'end': (4, 1)}, # Oct 1st Sun, Apr 1st Sun
}
def __init__(self, local_tz: Timezone = Timezone.UTC):
self.local_tz = local_tz
def utc_to_local(self, utc_time: datetime, target_tz: Timezone) -> datetime:
"""Convert UTC to target timezone."""
offset = timedelta(hours=target_tz.value)
return utc_time + offset
def local_to_utc(self, local_time: datetime, source_tz: Timezone) -> datetime:
"""Convert local time to UTC."""
offset = timedelta(hours=source_tz.value)
return local_time - offset
def get_session_times_utc(self) -> Dict[str, tuple]:
"""Get trading session times in UTC."""
return {
'Sydney': (21, 6), # 21:00 - 06:00 UTC
'Tokyo': (0, 9), # 00:00 - 09:00 UTC
'London': (7, 16), # 07:00 - 16:00 UTC
'New York': (12, 21), # 12:00 - 21:00 UTC
}
def is_session_active(self, session: str, utc_hour: int = None) -> bool:
"""Check if a trading session is active."""
if utc_hour is None:
utc_hour = datetime.utcnow().hour
sessions = self.get_session_times_utc()
if session not in sessions:
return False
start, end = sessions[session]
if start < end:
return start <= utc_hour < end
else: # Crosses midnight
return utc_hour >= start or utc_hour < end
class TradingScheduler:
"""Schedule trading activities across sessions."""
def __init__(self):
self.tz_converter = TimezoneConverter()
self.scheduled_tasks: List[Dict] = []
self.running = False
def add_task(self, name: str, callback: Callable,
schedule_type: str, **kwargs):
"""
Add a scheduled task.
schedule_type:
- 'interval': Run every N seconds (kwargs: interval_seconds)
- 'daily': Run at specific time (kwargs: hour, minute, timezone)
- 'session_start': Run when session opens (kwargs: session)
- 'session_end': Run when session closes (kwargs: session)
"""
task = {
'name': name,
'callback': callback,
'schedule_type': schedule_type,
'last_run': None,
'enabled': True,
**kwargs
}
self.scheduled_tasks.append(task)
def should_run_task(self, task: Dict, current_time: datetime) -> bool:
"""Check if task should run now."""
if not task['enabled']:
return False
schedule_type = task['schedule_type']
if schedule_type == 'interval':
if task['last_run'] is None:
return True
elapsed = (current_time - task['last_run']).total_seconds()
return elapsed >= task['interval_seconds']
elif schedule_type == 'daily':
target_hour = task['hour']
target_minute = task.get('minute', 0)
if current_time.hour == target_hour and current_time.minute == target_minute:
# Check if already run today
if task['last_run'] is None:
return True
return task['last_run'].date() < current_time.date()
elif schedule_type == 'session_start':
session = task['session']
sessions = self.tz_converter.get_session_times_utc()
if session in sessions:
start_hour, _ = sessions[session]
if current_time.hour == start_hour and current_time.minute < 5:
if task['last_run'] is None:
return True
return task['last_run'].date() < current_time.date()
return False
def run_pending(self) -> List[str]:
"""Run all pending tasks."""
current_time = datetime.utcnow()
executed = []
for task in self.scheduled_tasks:
if self.should_run_task(task, current_time):
try:
task['callback']()
task['last_run'] = current_time
executed.append(task['name'])
except Exception as e:
print(f"Task {task['name']} failed: {e}")
return executed
def get_status(self) -> pd.DataFrame:
"""Get status of all scheduled tasks."""
data = []
for task in self.scheduled_tasks:
data.append({
'Name': task['name'],
'Type': task['schedule_type'],
'Enabled': task['enabled'],
'Last Run': task['last_run']
})
return pd.DataFrame(data)
# Demonstrate scheduler
scheduler = TradingScheduler()
# Add various tasks
scheduler.add_task(
name='price_check',
callback=lambda: print("Checking prices..."),
schedule_type='interval',
interval_seconds=60
)
scheduler.add_task(
name='daily_report',
callback=lambda: print("Generating daily report..."),
schedule_type='daily',
hour=17, # 5 PM UTC
minute=0
)
scheduler.add_task(
name='london_open',
callback=lambda: print("London session opening..."),
schedule_type='session_start',
session='London'
)
print("Scheduled Tasks:")
print(scheduler.get_status())
# Run pending tasks
executed = scheduler.run_pending()
print(f"\nExecuted: {executed}")
class WeekendHandler:
"""Handle weekend market closures."""
# Forex closes Friday 21:00 UTC, opens Sunday 21:00 UTC
CLOSE_DAY = 4 # Friday
CLOSE_HOUR = 21
OPEN_DAY = 6 # Sunday
OPEN_HOUR = 21
def is_weekend(self, dt: datetime = None) -> bool:
"""Check if market is closed for weekend."""
if dt is None:
dt = datetime.utcnow()
weekday = dt.weekday()
hour = dt.hour
# Saturday
if weekday == 5:
return True
# Friday after close
if weekday == self.CLOSE_DAY and hour >= self.CLOSE_HOUR:
return True
# Sunday before open
if weekday == self.OPEN_DAY and hour < self.OPEN_HOUR:
return True
return False
def time_to_open(self, dt: datetime = None) -> timedelta:
"""Calculate time until market opens."""
if dt is None:
dt = datetime.utcnow()
if not self.is_weekend(dt):
return timedelta(0)
# Find next Sunday 21:00 UTC
days_to_sunday = (6 - dt.weekday()) % 7
if days_to_sunday == 0 and dt.hour >= self.OPEN_HOUR:
days_to_sunday = 7
next_open = dt.replace(hour=self.OPEN_HOUR, minute=0, second=0, microsecond=0)
next_open += timedelta(days=days_to_sunday)
return next_open - dt
def time_to_close(self, dt: datetime = None) -> timedelta:
"""Calculate time until market closes for weekend."""
if dt is None:
dt = datetime.utcnow()
if self.is_weekend(dt):
return timedelta(0)
# Find next Friday 21:00 UTC
days_to_friday = (4 - dt.weekday()) % 7
if days_to_friday == 0 and dt.hour >= self.CLOSE_HOUR:
days_to_friday = 7
next_close = dt.replace(hour=self.CLOSE_HOUR, minute=0, second=0, microsecond=0)
next_close += timedelta(days=days_to_friday)
return next_close - dt
# Test weekend handler
weekend = WeekendHandler()
# Test different times
test_times = [
datetime(2024, 1, 12, 20, 0), # Friday 20:00 - open
datetime(2024, 1, 12, 22, 0), # Friday 22:00 - closed
datetime(2024, 1, 13, 12, 0), # Saturday - closed
datetime(2024, 1, 14, 20, 0), # Sunday 20:00 - closed
datetime(2024, 1, 14, 22, 0), # Sunday 22:00 - open
]
print("Weekend Status Check:")
for dt in test_times:
is_closed = weekend.is_weekend(dt)
print(f"{dt.strftime('%A %H:%M')}: {'CLOSED' if is_closed else 'OPEN'}")
Section 13.2: Alerts & Notifications
Comprehensive alerting for price movements, signals, and system events.
class AlertLevel(Enum):
INFO = "info"
WARNING = "warning"
CRITICAL = "critical"
@dataclass
class Alert:
"""Represents a single alert."""
id: str
level: AlertLevel
category: str
message: str
timestamp: datetime
data: Dict = field(default_factory=dict)
acknowledged: bool = False
def to_dict(self) -> Dict:
return {
'id': self.id,
'level': self.level.value,
'category': self.category,
'message': self.message,
'timestamp': self.timestamp.isoformat(),
'data': self.data
}
class AlertManager:
"""Manage alerts and notifications."""
def __init__(self, max_alerts: int = 1000):
self.alerts: deque = deque(maxlen=max_alerts)
self.alert_count = 0
self.handlers: List[Callable] = []
self.suppression_rules: Dict[str, datetime] = {}
def add_handler(self, handler: Callable):
"""Add a notification handler (email, SMS, etc.)."""
self.handlers.append(handler)
def suppress(self, category: str, duration_minutes: int):
"""Suppress alerts of a category for a duration."""
self.suppression_rules[category] = datetime.now() + timedelta(minutes=duration_minutes)
def _is_suppressed(self, category: str) -> bool:
"""Check if category is suppressed."""
if category in self.suppression_rules:
if datetime.now() < self.suppression_rules[category]:
return True
else:
del self.suppression_rules[category]
return False
def create_alert(self, level: AlertLevel, category: str,
message: str, data: Dict = None) -> Optional[Alert]:
"""Create and dispatch an alert."""
if self._is_suppressed(category):
return None
self.alert_count += 1
alert = Alert(
id=f"ALT-{self.alert_count:06d}",
level=level,
category=category,
message=message,
timestamp=datetime.now(),
data=data or {}
)
self.alerts.append(alert)
# Dispatch to handlers
for handler in self.handlers:
try:
handler(alert)
except Exception as e:
print(f"Handler error: {e}")
return alert
def get_active_alerts(self, level: AlertLevel = None) -> List[Alert]:
"""Get unacknowledged alerts."""
alerts = [a for a in self.alerts if not a.acknowledged]
if level:
alerts = [a for a in alerts if a.level == level]
return alerts
def acknowledge(self, alert_id: str):
"""Acknowledge an alert."""
for alert in self.alerts:
if alert.id == alert_id:
alert.acknowledged = True
break
def get_summary(self) -> Dict:
"""Get alert summary."""
active = self.get_active_alerts()
return {
'total': len(self.alerts),
'active': len(active),
'critical': len([a for a in active if a.level == AlertLevel.CRITICAL]),
'warning': len([a for a in active if a.level == AlertLevel.WARNING]),
'info': len([a for a in active if a.level == AlertLevel.INFO])
}
class PriceAlertMonitor:
"""Monitor prices and generate alerts."""
def __init__(self, alert_manager: AlertManager):
self.alert_manager = alert_manager
self.price_alerts: List[Dict] = []
def add_price_alert(self, instrument: str, condition: str,
price: float, message: str = None):
"""
Add a price alert.
condition: 'above', 'below', 'cross_above', 'cross_below'
"""
alert = {
'instrument': instrument,
'condition': condition,
'price': price,
'message': message or f"{instrument} {condition} {price}",
'triggered': False,
'last_price': None
}
self.price_alerts.append(alert)
def check_prices(self, current_prices: Dict[str, float]):
"""Check all price alerts against current prices."""
for alert in self.price_alerts:
if alert['triggered']:
continue
instrument = alert['instrument']
if instrument not in current_prices:
continue
current = current_prices[instrument]
target = alert['price']
condition = alert['condition']
last = alert['last_price']
should_trigger = False
if condition == 'above' and current > target:
should_trigger = True
elif condition == 'below' and current < target:
should_trigger = True
elif condition == 'cross_above' and last is not None:
if last <= target and current > target:
should_trigger = True
elif condition == 'cross_below' and last is not None:
if last >= target and current < target:
should_trigger = True
if should_trigger:
alert['triggered'] = True
self.alert_manager.create_alert(
level=AlertLevel.INFO,
category='price_alert',
message=alert['message'],
data={'instrument': instrument, 'price': current, 'target': target}
)
alert['last_price'] = current
class SignalAlertMonitor:
"""Monitor trading signals and generate alerts."""
def __init__(self, alert_manager: AlertManager):
self.alert_manager = alert_manager
def on_signal(self, signal: Dict):
"""Handle a new trading signal."""
instrument = signal.get('instrument', 'Unknown')
direction = signal.get('direction', 'Unknown')
strength = signal.get('strength', 0)
# Determine alert level based on signal strength
if strength > 0.8:
level = AlertLevel.CRITICAL
elif strength > 0.5:
level = AlertLevel.WARNING
else:
level = AlertLevel.INFO
self.alert_manager.create_alert(
level=level,
category='trading_signal',
message=f"{direction.upper()} signal on {instrument} (strength: {strength:.2f})",
data=signal
)
class ErrorAlertMonitor:
"""Monitor system errors and generate alerts."""
def __init__(self, alert_manager: AlertManager):
self.alert_manager = alert_manager
self.error_counts: Dict[str, int] = {}
self.threshold = 3 # Alert after 3 errors of same type
def on_error(self, error_type: str, error_message: str, context: Dict = None):
"""Handle an error."""
# Track error count
self.error_counts[error_type] = self.error_counts.get(error_type, 0) + 1
count = self.error_counts[error_type]
# Determine level based on frequency
if count >= self.threshold * 3:
level = AlertLevel.CRITICAL
elif count >= self.threshold:
level = AlertLevel.WARNING
else:
level = AlertLevel.INFO
self.alert_manager.create_alert(
level=level,
category='system_error',
message=f"{error_type}: {error_message} (count: {count})",
data={'error_type': error_type, 'count': count, 'context': context or {}}
)
# Demonstrate alert system
alert_manager = AlertManager()
# Add console handler
def console_handler(alert: Alert):
level_icon = {'info': 'i', 'warning': '!', 'critical': 'X'}[alert.level.value]
print(f"[{level_icon}] {alert.timestamp.strftime('%H:%M:%S')} - {alert.message}")
alert_manager.add_handler(console_handler)
# Setup monitors
price_monitor = PriceAlertMonitor(alert_manager)
signal_monitor = SignalAlertMonitor(alert_manager)
error_monitor = ErrorAlertMonitor(alert_manager)
# Add price alerts
price_monitor.add_price_alert('EURUSD', 'above', 1.0900, 'EURUSD broke above 1.0900!')
price_monitor.add_price_alert('GBPUSD', 'below', 1.2600, 'GBPUSD fell below 1.2600!')
# Simulate price updates
print("\n--- Price Updates ---")
price_monitor.check_prices({'EURUSD': 1.0850, 'GBPUSD': 1.2650})
price_monitor.check_prices({'EURUSD': 1.0920, 'GBPUSD': 1.2580})
# Simulate signal
print("\n--- Trading Signal ---")
signal_monitor.on_signal({
'instrument': 'EURUSD',
'direction': 'buy',
'strength': 0.85,
'entry': 1.0920
})
# Summary
print(f"\nAlert Summary: {alert_manager.get_summary()}")
Section 13.3: Performance Tracking
Real-time performance monitoring and reporting.
@dataclass
class TradeRecord:
"""Record of a completed trade."""
trade_id: str
instrument: str
direction: str
entry_price: float
exit_price: float
units: float
entry_time: datetime
exit_time: datetime
pnl: float
pnl_pips: float
@property
def duration(self) -> timedelta:
return self.exit_time - self.entry_time
@property
def is_winner(self) -> bool:
return self.pnl > 0
class PerformanceTracker:
"""Track and analyze trading performance."""
def __init__(self, initial_balance: float):
self.initial_balance = initial_balance
self.current_balance = initial_balance
self.trades: List[TradeRecord] = []
self.equity_curve: List[Dict] = [{
'timestamp': datetime.now(),
'balance': initial_balance,
'equity': initial_balance
}]
self.daily_pnl: Dict[str, float] = {}
def record_trade(self, trade: TradeRecord):
"""Record a completed trade."""
self.trades.append(trade)
self.current_balance += trade.pnl
# Update equity curve
self.equity_curve.append({
'timestamp': trade.exit_time,
'balance': self.current_balance,
'equity': self.current_balance
})
# Update daily P&L
date_key = trade.exit_time.strftime('%Y-%m-%d')
self.daily_pnl[date_key] = self.daily_pnl.get(date_key, 0) + trade.pnl
def get_statistics(self) -> Dict:
"""Calculate comprehensive statistics."""
if not self.trades:
return {'trades': 0}
winners = [t for t in self.trades if t.is_winner]
losers = [t for t in self.trades if not t.is_winner]
total_pnl = sum(t.pnl for t in self.trades)
avg_win = np.mean([t.pnl for t in winners]) if winners else 0
avg_loss = np.mean([t.pnl for t in losers]) if losers else 0
# Calculate max drawdown
equity = [e['equity'] for e in self.equity_curve]
peak = equity[0]
max_dd = 0
for e in equity:
if e > peak:
peak = e
dd = (peak - e) / peak
max_dd = max(max_dd, dd)
# Calculate profit factor
gross_profit = sum(t.pnl for t in winners)
gross_loss = abs(sum(t.pnl for t in losers))
profit_factor = gross_profit / gross_loss if gross_loss > 0 else float('inf')
return {
'total_trades': len(self.trades),
'winners': len(winners),
'losers': len(losers),
'win_rate': len(winners) / len(self.trades) * 100,
'total_pnl': total_pnl,
'return_pct': (self.current_balance / self.initial_balance - 1) * 100,
'avg_win': avg_win,
'avg_loss': avg_loss,
'profit_factor': profit_factor,
'max_drawdown_pct': max_dd * 100,
'avg_trade_duration': np.mean([t.duration.total_seconds() / 3600 for t in self.trades])
}
class RealTimePnLTracker:
"""Track P&L in real-time."""
def __init__(self, alert_manager: AlertManager = None):
self.positions: Dict[str, Dict] = {}
self.realized_pnl = 0.0
self.alert_manager = alert_manager
self.daily_limit = -1000 # Max daily loss
def open_position(self, instrument: str, units: float,
entry_price: float, pip_value: float = 10.0):
"""Record position opening."""
self.positions[instrument] = {
'units': units,
'entry_price': entry_price,
'pip_value': pip_value,
'current_price': entry_price,
'unrealized_pnl': 0.0
}
def update_price(self, instrument: str, current_price: float):
"""Update price and recalculate unrealized P&L."""
if instrument not in self.positions:
return
pos = self.positions[instrument]
pos['current_price'] = current_price
# Calculate unrealized P&L
price_diff = current_price - pos['entry_price']
if pos['units'] < 0: # Short position
price_diff = -price_diff
pips = price_diff * 10000 # Assuming 4 decimal places
pos['unrealized_pnl'] = pips * pos['pip_value'] * abs(pos['units']) / 100000
def close_position(self, instrument: str) -> float:
"""Close position and realize P&L."""
if instrument not in self.positions:
return 0.0
pnl = self.positions[instrument]['unrealized_pnl']
self.realized_pnl += pnl
del self.positions[instrument]
# Check daily limit
self._check_daily_limit()
return pnl
def _check_daily_limit(self):
"""Check if daily loss limit is breached."""
total_pnl = self.get_total_pnl()
if total_pnl < self.daily_limit and self.alert_manager:
self.alert_manager.create_alert(
level=AlertLevel.CRITICAL,
category='risk_limit',
message=f"Daily loss limit breached: ${total_pnl:.2f}",
data={'realized': self.realized_pnl, 'unrealized': self.get_unrealized_pnl()}
)
def get_unrealized_pnl(self) -> float:
"""Get total unrealized P&L."""
return sum(p['unrealized_pnl'] for p in self.positions.values())
def get_total_pnl(self) -> float:
"""Get total P&L (realized + unrealized)."""
return self.realized_pnl + self.get_unrealized_pnl()
def get_position_summary(self) -> pd.DataFrame:
"""Get summary of all positions."""
if not self.positions:
return pd.DataFrame()
data = []
for inst, pos in self.positions.items():
data.append({
'Instrument': inst,
'Units': pos['units'],
'Entry': pos['entry_price'],
'Current': pos['current_price'],
'Unrealized P&L': pos['unrealized_pnl']
})
return pd.DataFrame(data)
# Demonstrate performance tracking
tracker = PerformanceTracker(initial_balance=10000)
# Simulate some trades
sample_trades = [
TradeRecord('T001', 'EURUSD', 'long', 1.0800, 1.0850, 10000,
datetime(2024,1,1,10,0), datetime(2024,1,1,14,0), 50, 50),
TradeRecord('T002', 'GBPUSD', 'short', 1.2700, 1.2650, 10000,
datetime(2024,1,2,9,0), datetime(2024,1,2,16,0), 50, 50),
TradeRecord('T003', 'USDJPY', 'long', 148.00, 147.50, 10000,
datetime(2024,1,3,8,0), datetime(2024,1,3,12,0), -33, -50),
TradeRecord('T004', 'EURUSD', 'long', 1.0850, 1.0920, 10000,
datetime(2024,1,4,10,0), datetime(2024,1,5,10,0), 70, 70),
]
for trade in sample_trades:
tracker.record_trade(trade)
# Get statistics
stats = tracker.get_statistics()
print("Performance Statistics:")
print("-" * 40)
for key, value in stats.items():
if isinstance(value, float):
print(f"{key}: {value:.2f}")
else:
print(f"{key}: {value}")
class DailySummaryReport:
"""Generate daily performance summaries."""
def __init__(self, tracker: PerformanceTracker):
self.tracker = tracker
def generate(self, date: str = None) -> str:
"""Generate daily summary report."""
if date is None:
date = datetime.now().strftime('%Y-%m-%d')
# Get trades for the date
day_trades = [t for t in self.tracker.trades
if t.exit_time.strftime('%Y-%m-%d') == date]
if not day_trades:
return f"No trades on {date}"
day_pnl = sum(t.pnl for t in day_trades)
winners = [t for t in day_trades if t.is_winner]
lines = [
f"\n{'='*50}",
f"DAILY PERFORMANCE SUMMARY - {date}",
f"{'='*50}",
f"",
f"Trades: {len(day_trades)}",
f"Winners: {len(winners)} ({len(winners)/len(day_trades)*100:.1f}%)",
f"Losers: {len(day_trades) - len(winners)}",
f"",
f"Daily P&L: ${day_pnl:,.2f}",
f"Account Balance: ${self.tracker.current_balance:,.2f}",
f"",
f"Trade Details:",
f"{'-'*50}"
]
for t in day_trades:
status = 'WIN' if t.is_winner else 'LOSS'
lines.append(f" {t.instrument} {t.direction}: {t.pnl_pips:+.1f} pips (${t.pnl:+.2f}) [{status}]")
lines.append(f"{'='*50}")
return "\n".join(lines)
# Generate daily report
report = DailySummaryReport(tracker)
print(report.generate('2024-01-01'))
Section 13.4: System Health
Monitor system health and implement failover logic.
class HealthStatus(Enum):
HEALTHY = "healthy"
DEGRADED = "degraded"
CRITICAL = "critical"
OFFLINE = "offline"
@dataclass
class HealthCheck:
"""Result of a health check."""
name: str
status: HealthStatus
message: str
latency_ms: float = 0.0
last_check: datetime = field(default_factory=datetime.now)
class SystemHealthMonitor:
"""Comprehensive system health monitoring."""
def __init__(self, alert_manager: AlertManager = None):
self.alert_manager = alert_manager
self.checks: Dict[str, HealthCheck] = {}
self.check_history: Dict[str, List[HealthCheck]] = {}
self.max_history = 100
def register_check(self, name: str, checker: Callable[[], HealthCheck]):
"""Register a health check function."""
self.checks[name] = None
self.check_history[name] = []
self._checkers = getattr(self, '_checkers', {})
self._checkers[name] = checker
def run_check(self, name: str) -> HealthCheck:
"""Run a specific health check."""
if name not in self._checkers:
return HealthCheck(name, HealthStatus.OFFLINE, "Check not registered")
start = time_module.time()
try:
result = self._checkers[name]()
result.latency_ms = (time_module.time() - start) * 1000
except Exception as e:
result = HealthCheck(name, HealthStatus.CRITICAL, str(e))
result.latency_ms = (time_module.time() - start) * 1000
# Store result
self.checks[name] = result
self.check_history[name].append(result)
if len(self.check_history[name]) > self.max_history:
self.check_history[name].pop(0)
# Alert on status changes
self._check_status_change(name, result)
return result
def run_all_checks(self) -> Dict[str, HealthCheck]:
"""Run all registered health checks."""
for name in self._checkers:
self.run_check(name)
return self.checks
def _check_status_change(self, name: str, current: HealthCheck):
"""Check for status changes and alert."""
history = self.check_history[name]
if len(history) < 2:
return
previous = history[-2]
if previous.status != current.status and self.alert_manager:
level = AlertLevel.CRITICAL if current.status in [HealthStatus.CRITICAL, HealthStatus.OFFLINE] else AlertLevel.WARNING
self.alert_manager.create_alert(
level=level,
category='health_status',
message=f"{name} status changed: {previous.status.value} -> {current.status.value}",
data={'check': name, 'previous': previous.status.value, 'current': current.status.value}
)
def get_overall_status(self) -> HealthStatus:
"""Get overall system health status."""
if not self.checks:
return HealthStatus.OFFLINE
statuses = [c.status for c in self.checks.values() if c is not None]
if HealthStatus.OFFLINE in statuses or HealthStatus.CRITICAL in statuses:
return HealthStatus.CRITICAL
elif HealthStatus.DEGRADED in statuses:
return HealthStatus.DEGRADED
else:
return HealthStatus.HEALTHY
def get_uptime(self, name: str) -> float:
"""Calculate uptime percentage for a check."""
history = self.check_history.get(name, [])
if not history:
return 0.0
healthy = len([h for h in history if h.status == HealthStatus.HEALTHY])
return healthy / len(history) * 100
def generate_report(self) -> str:
"""Generate health status report."""
lines = [
f"\n{'='*60}",
f"SYSTEM HEALTH REPORT - {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}",
f"{'='*60}",
f"",
f"Overall Status: {self.get_overall_status().value.upper()}",
f"",
f"Component Status:",
f"{'-'*60}"
]
status_icons = {
HealthStatus.HEALTHY: '[OK]',
HealthStatus.DEGRADED: '[!!]',
HealthStatus.CRITICAL: '[XX]',
HealthStatus.OFFLINE: '[--]'
}
for name, check in self.checks.items():
if check:
icon = status_icons[check.status]
uptime = self.get_uptime(name)
lines.append(f" {icon} {name}: {check.message} (latency: {check.latency_ms:.1f}ms, uptime: {uptime:.1f}%)")
else:
lines.append(f" [--] {name}: Not checked")
lines.append(f"{'='*60}")
return "\n".join(lines)
class DataQualityChecker:
"""Check data feed quality."""
def __init__(self):
self.last_prices: Dict[str, Dict] = {}
self.gap_threshold = 0.01 # 1% price gap is suspicious
self.stale_threshold = 60 # Seconds before data is considered stale
def check_price(self, instrument: str, price: float, timestamp: datetime) -> Dict:
"""Check price data quality."""
issues = []
if instrument in self.last_prices:
last = self.last_prices[instrument]
# Check for gaps
change = abs(price / last['price'] - 1)
if change > self.gap_threshold:
issues.append(f"Large price gap: {change*100:.2f}%")
# Check for stale data
time_diff = (timestamp - last['timestamp']).total_seconds()
if time_diff > self.stale_threshold:
issues.append(f"Data was stale for {time_diff:.0f}s")
# Update last price
self.last_prices[instrument] = {
'price': price,
'timestamp': timestamp
}
return {
'instrument': instrument,
'price': price,
'issues': issues,
'is_valid': len(issues) == 0
}
class FailoverManager:
"""Manage failover between primary and backup systems."""
def __init__(self):
self.primary_active = True
self.failover_count = 0
self.last_failover: datetime = None
self.cooldown_minutes = 5
def trigger_failover(self, reason: str) -> bool:
"""Trigger failover to backup system."""
# Check cooldown
if self.last_failover:
elapsed = (datetime.now() - self.last_failover).total_seconds() / 60
if elapsed < self.cooldown_minutes:
return False
self.primary_active = not self.primary_active
self.failover_count += 1
self.last_failover = datetime.now()
system = "backup" if not self.primary_active else "primary"
print(f"FAILOVER: Switched to {system} system. Reason: {reason}")
return True
def get_active_system(self) -> str:
"""Get currently active system."""
return "primary" if self.primary_active else "backup"
# Demonstrate health monitoring
health_monitor = SystemHealthMonitor(alert_manager)
# Register health checks
def check_broker_connection():
# Simulated check
return HealthCheck("broker_connection", HealthStatus.HEALTHY, "Connected to broker")
def check_data_feed():
return HealthCheck("data_feed", HealthStatus.HEALTHY, "Data feed active")
def check_strategy_engine():
return HealthCheck("strategy_engine", HealthStatus.HEALTHY, "Strategy running")
def check_database():
# Simulate degraded state
return HealthCheck("database", HealthStatus.DEGRADED, "High latency detected")
health_monitor.register_check("broker_connection", check_broker_connection)
health_monitor.register_check("data_feed", check_data_feed)
health_monitor.register_check("strategy_engine", check_strategy_engine)
health_monitor.register_check("database", check_database)
# Run all checks
health_monitor.run_all_checks()
# Generate report
print(health_monitor.generate_report())
Exercises
Exercise 1: Session-Based Scheduler (Guided)
Create a scheduler that runs different logic for different trading sessions.
class SessionBasedScheduler:
"""Run different tasks based on trading session."""
def __init__(self):
self.session_tasks: Dict[str, List[Callable]] = {
'Sydney': [],
'Tokyo': [],
'London': [],
'New_York': []
}
self.tz_converter = TimezoneConverter()
def add_session_task(self, session: str, task: Callable):
"""Add a task for a specific session."""
if session in self.session_tasks:
self.session_tasks[session].______(task) # Add task to list
def get_current_sessions(self) -> List[str]:
"""Get currently active sessions."""
current_hour = datetime.utcnow().hour
active = []
for session in ['Sydney', 'Tokyo', 'London', 'New_York']:
if self.tz_converter.is_session_active(session.replace('_', ' '), current_hour):
active.append(______) # Append session name
return active
def run_session_tasks(self) -> Dict[str, int]:
"""Run all tasks for currently active sessions."""
results = {}
active_sessions = self.get_current_sessions()
for session in active_sessions:
tasks = self.session_tasks.get(session, [])
executed = 0
for task in ______: # Iterate over tasks
try:
task()
executed += 1
except Exception as e:
print(f"Task error in {session}: {e}")
results[session] = executed
return results
# Test the scheduler
session_scheduler = SessionBasedScheduler()
session_scheduler.add_session_task('London', lambda: print(" Checking EUR pairs..."))
session_scheduler.add_session_task('New_York', lambda: print(" Checking USD pairs..."))
print(f"Active sessions: {session_scheduler.get_current_sessions()}")
print("Running session tasks:")
results = session_scheduler.run_session_tasks()
print(f"Results: {results}")
Exercise 2: Multi-Channel Alert System (Guided)
Create an alert system with multiple notification channels.
class NotificationChannel:
"""Base class for notification channels."""
def send(self, alert: Alert) -> bool:
raise NotImplementedError
class ConsoleChannel(NotificationChannel):
"""Print alerts to console."""
def send(self, alert: Alert) -> bool:
print(f"[{alert.level.value.upper()}] {alert.message}")
return True
class EmailChannel(NotificationChannel):
"""Send alerts via email (simulated)."""
def __init__(self, recipient: str):
self.recipient = recipient
def send(self, alert: Alert) -> bool:
print(f"EMAIL to {self.recipient}: {alert.message}")
return ______ # Return success status
class MultiChannelAlertManager:
"""Alert manager with multiple notification channels."""
def __init__(self):
self.channels: Dict[str, NotificationChannel] = {}
self.routing_rules: Dict[AlertLevel, List[str]] = {
AlertLevel.INFO: [],
AlertLevel.WARNING: [],
AlertLevel.CRITICAL: []
}
self.alert_count = 0
def add_channel(self, name: str, channel: NotificationChannel):
"""Add a notification channel."""
self.channels[______] = channel # Store channel by name
def set_routing(self, level: AlertLevel, channels: List[str]):
"""Set which channels receive which alert levels."""
self.routing_rules[level] = channels
def send_alert(self, level: AlertLevel, message: str, data: Dict = None):
"""Create and send alert through appropriate channels."""
self.alert_count += 1
alert = Alert(
id=f"ALT-{self.alert_count:06d}",
level=level,
category='general',
message=message,
timestamp=datetime.now(),
data=data or {}
)
# Get channels for this level
channel_names = self.routing_rules.get(level, [])
for name in channel_names:
if name in self.______: # Access channels dict
self.channels[name].send(alert)
# Test multi-channel alerts
mcam = MultiChannelAlertManager()
mcam.add_channel('console', ConsoleChannel())
mcam.add_channel('email', EmailChannel('trader@example.com'))
# Route INFO to console only, WARNING and CRITICAL to both
mcam.set_routing(AlertLevel.INFO, ['console'])
mcam.set_routing(AlertLevel.WARNING, ['console', 'email'])
mcam.set_routing(AlertLevel.CRITICAL, ['console', 'email'])
print("\nSending alerts:")
mcam.send_alert(AlertLevel.INFO, "System started")
mcam.send_alert(AlertLevel.CRITICAL, "Connection lost!")
Exercise 3: Performance Dashboard (Guided)
Create a real-time performance dashboard data structure.
class PerformanceDashboard:
"""Real-time performance dashboard."""
def __init__(self, tracker: PerformanceTracker):
self.tracker = tracker
self.refresh_interval = 60 # seconds
self.last_refresh = None
def get_dashboard_data(self) -> Dict:
"""Get all dashboard data."""
stats = self.tracker.get_statistics()
return {
'summary': self._get_summary(stats),
'recent_trades': self._get_recent_trades(5),
'daily_pnl': self._get_daily_pnl(),
'updated_at': datetime.now().isoformat()
}
def _get_summary(self, stats: Dict) -> Dict:
"""Get summary metrics."""
return {
'total_pnl': stats.get('total_pnl', 0),
'win_rate': stats.get('______', 0), # Get win rate
'total_trades': stats.get('total_trades', 0),
'profit_factor': stats.get('profit_factor', 0)
}
def _get_recent_trades(self, n: int) -> List[Dict]:
"""Get N most recent trades."""
trades = self.tracker.trades[-n:] if self.tracker.trades else []
return [
{
'instrument': t.instrument,
'direction': t.direction,
'pnl': t.______, # Get P&L
'time': t.exit_time.isoformat()
}
for t in reversed(trades)
]
def _get_daily_pnl(self) -> List[Dict]:
"""Get daily P&L for charting."""
return [
{'date': date, 'pnl': pnl}
for date, pnl in self.tracker.daily_pnl.______()
] # Iterate over items
# Create dashboard
dashboard = PerformanceDashboard(tracker)
data = dashboard.get_dashboard_data()
print("\nDashboard Data:")
print(json.dumps(data, indent=2, default=str))
Exercise 4: 24/7 Task Runner
Build a complete 24/7 task runner with weekend handling.
# Exercise 4: Build a TwentyFourSevenRunner class that:
# 1. Runs continuously with configurable sleep interval
# 2. Automatically pauses during weekends
# 3. Executes different tasks based on trading session
# 4. Logs all activity
# 5. Handles graceful shutdown
# Your code here
Exercise 5: Advanced Alert Rules
Create an advanced alert rule engine.
# Exercise 5: Build an AlertRuleEngine class that:
# 1. Supports complex conditions (AND, OR, NOT)
# 2. Has rate limiting per rule (max N alerts per hour)
# 3. Supports time-based rules (only alert during certain hours)
# 4. Can escalate alerts if condition persists
# 5. Generates alert statistics
# Your code here
Exercise 6: System Recovery
Build an automatic system recovery manager.
# Exercise 6: Build a SystemRecoveryManager class that:
# 1. Detects when components are unhealthy
# 2. Attempts automatic recovery (reconnect, restart)
# 3. Implements exponential backoff for retries
# 4. Triggers failover after N failed attempts
# 5. Maintains recovery audit log
# Your code here
Module Project: Production Monitoring System
Build a comprehensive production monitoring system.
class ProductionMonitoringSystem:
"""
Complete production monitoring system.
Integrates:
- 24/7 scheduling
- Multi-channel alerting
- Performance tracking
- Health monitoring
- Failover management
"""
def __init__(self, config: Dict):
self.config = config
# Initialize components
self.alert_manager = AlertManager()
self.scheduler = TradingScheduler()
self.health_monitor = SystemHealthMonitor(self.alert_manager)
self.performance_tracker = PerformanceTracker(config.get('initial_balance', 10000))
self.pnl_tracker = RealTimePnLTracker(self.alert_manager)
self.weekend_handler = WeekendHandler()
self.failover = FailoverManager()
# State
self.running = False
self.start_time = None
# Setup
self._setup_alert_handlers()
self._setup_scheduled_tasks()
self._setup_health_checks()
def _setup_alert_handlers(self):
"""Setup notification channels."""
def log_handler(alert: Alert):
logging.info(f"[{alert.level.value}] {alert.message}")
self.alert_manager.add_handler(log_handler)
def _setup_scheduled_tasks(self):
"""Setup scheduled monitoring tasks."""
# Health check every minute
self.scheduler.add_task(
name='health_check',
callback=self._run_health_check,
schedule_type='interval',
interval_seconds=60
)
# Daily summary at end of NY session
self.scheduler.add_task(
name='daily_summary',
callback=self._generate_daily_summary,
schedule_type='daily',
hour=21,
minute=0
)
def _setup_health_checks(self):
"""Register health checks."""
self.health_monitor.register_check(
'system_status',
lambda: HealthCheck('system_status', HealthStatus.HEALTHY, 'System operational')
)
def _run_health_check(self):
"""Execute health check cycle."""
results = self.health_monitor.run_all_checks()
overall = self.health_monitor.get_overall_status()
if overall == HealthStatus.CRITICAL:
self.failover.trigger_failover("Critical health status")
def _generate_daily_summary(self):
"""Generate and send daily summary."""
report = DailySummaryReport(self.performance_tracker)
summary = report.generate()
self.alert_manager.create_alert(
level=AlertLevel.INFO,
category='daily_summary',
message="Daily Summary Generated",
data={'summary': summary}
)
def start(self):
"""Start the monitoring system."""
self.running = True
self.start_time = datetime.now()
self.alert_manager.create_alert(
level=AlertLevel.INFO,
category='system',
message="Production monitoring system started"
)
def stop(self):
"""Stop the monitoring system."""
self.running = False
self.alert_manager.create_alert(
level=AlertLevel.INFO,
category='system',
message="Production monitoring system stopped"
)
def run_cycle(self):
"""Run one monitoring cycle."""
if not self.running:
return
# Check if weekend
if self.weekend_handler.is_weekend():
return
# Run scheduled tasks
executed = self.scheduler.run_pending()
return executed
def get_status(self) -> Dict:
"""Get comprehensive system status."""
uptime = (datetime.now() - self.start_time).total_seconds() if self.start_time else 0
return {
'running': self.running,
'uptime_hours': uptime / 3600,
'health': self.health_monitor.get_overall_status().value,
'alerts': self.alert_manager.get_summary(),
'performance': self.performance_tracker.get_statistics(),
'active_system': self.failover.get_active_system(),
'is_weekend': self.weekend_handler.is_weekend()
}
# Demonstrate the production monitoring system
config = {
'initial_balance': 10000,
'alert_email': 'trader@example.com'
}
monitoring_system = ProductionMonitoringSystem(config)
# Start the system
monitoring_system.start()
# Add some sample trades to the tracker
for trade in sample_trades:
monitoring_system.performance_tracker.record_trade(trade)
# Run a few cycles
for i in range(3):
monitoring_system.run_cycle()
# Get status
status = monitoring_system.get_status()
print("\nSystem Status:")
print(json.dumps(status, indent=2, default=str))
# Generate health report
print(monitoring_system.health_monitor.generate_report())
Key Takeaways
-
24/7 Scheduling: Forex requires timezone-aware scheduling with session and weekend handling
-
Alerting: Multi-channel alerts with routing, suppression, and escalation are essential
-
Performance Tracking: Real-time P&L and comprehensive statistics enable informed decisions
-
Health Monitoring: Proactive monitoring prevents issues and enables automatic recovery
-
Failover: Automatic failover between systems ensures continuous operation
Next Module: Module 14 - Trading Psychology & Journaling
Module 14: Trading Psychology & Journaling
| Duration | ~2.5 hours |
| Skill Level | Advanced |
| Prerequisites | Modules 11-13 |
Learning Objectives
By the end of this module, you will be able to: - Identify and mitigate common trading biases - Build a comprehensive automated trading journal - Implement systematic performance review processes - Create continuous improvement frameworks
Prerequisites
- Completed Part 3 and Modules 12-13
- Understanding of trading performance metrics
- Familiarity with data analysis and visualization
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from datetime import datetime, timedelta
from typing import Dict, List, Optional, Tuple
from dataclasses import dataclass, field
from enum import Enum
import json
from collections import defaultdict
Section 14.1: Trading Psychology
Understanding and managing psychological biases is crucial for trading success.
Common Trading Biases
Cognitive Biases: - Confirmation Bias: Seeking information that confirms existing beliefs - Recency Bias: Overweighting recent events - Anchoring: Fixating on specific price levels - Hindsight Bias: "I knew it all along"
Emotional Biases: - Loss Aversion: Pain of losses > pleasure of gains (2x typically) - Overconfidence: Overestimating prediction ability - FOMO: Fear of missing out on trades - Revenge Trading: Trading to recover losses
Leverage Psychology: - Higher leverage amplifies emotional responses - Small account moves feel larger - Can lead to premature exit or excessive risk
class TradingBias(Enum):
"""Common trading biases."""
CONFIRMATION = "confirmation_bias"
RECENCY = "recency_bias"
ANCHORING = "anchoring_bias"
LOSS_AVERSION = "loss_aversion"
OVERCONFIDENCE = "overconfidence"
FOMO = "fear_of_missing_out"
REVENGE_TRADING = "revenge_trading"
DISPOSITION_EFFECT = "disposition_effect" # Selling winners too early, holding losers
@dataclass
class BiasIndicator:
"""Indicator for detecting a specific bias."""
bias: TradingBias
description: str
detection_rule: str
mitigation: str
class BiasDetector:
"""Detect potential trading biases from trade data."""
def __init__(self):
self.bias_indicators = self._setup_indicators()
def _setup_indicators(self) -> Dict[TradingBias, BiasIndicator]:
"""Setup bias detection indicators."""
return {
TradingBias.LOSS_AVERSION: BiasIndicator(
bias=TradingBias.LOSS_AVERSION,
description="Holding losing trades too long while cutting winners short",
detection_rule="Average losing trade duration > 2x average winning trade duration",
mitigation="Use strict stop losses; define exit rules before entry"
),
TradingBias.OVERCONFIDENCE: BiasIndicator(
bias=TradingBias.OVERCONFIDENCE,
description="Increasing position sizes after winning streak",
detection_rule="Position size increases >50% after 3+ consecutive wins",
mitigation="Use fixed position sizing rules; scale based on account, not recent results"
),
TradingBias.REVENGE_TRADING: BiasIndicator(
bias=TradingBias.REVENGE_TRADING,
description="Increased trading frequency after losses",
detection_rule="Trade count increases >100% in hour following losing trade",
mitigation="Implement mandatory cooldown periods after losses"
),
TradingBias.DISPOSITION_EFFECT: BiasIndicator(
bias=TradingBias.DISPOSITION_EFFECT,
description="Selling winners early, holding losers",
detection_rule="Average winning trade R:R < 1.0 while holding losers beyond stop",
mitigation="Let winners run; use trailing stops"
),
TradingBias.FOMO: BiasIndicator(
bias=TradingBias.FOMO,
description="Entering trades after missing initial move",
detection_rule="Entry price >80% of way to resistance/support after move started",
mitigation="Wait for pullbacks; define entry zones before market opens"
)
}
def analyze_trades(self, trades: List[Dict]) -> List[Dict]:
"""Analyze trades for potential biases."""
detected_biases = []
if not trades:
return detected_biases
# Check for loss aversion
winners = [t for t in trades if t.get('pnl', 0) > 0]
losers = [t for t in trades if t.get('pnl', 0) <= 0]
if winners and losers:
avg_win_duration = np.mean([t.get('duration_hours', 1) for t in winners])
avg_loss_duration = np.mean([t.get('duration_hours', 1) for t in losers])
if avg_loss_duration > 2 * avg_win_duration:
detected_biases.append({
'bias': TradingBias.LOSS_AVERSION,
'severity': 'high' if avg_loss_duration > 3 * avg_win_duration else 'medium',
'evidence': f"Avg loss duration ({avg_loss_duration:.1f}h) > 2x avg win duration ({avg_win_duration:.1f}h)"
})
# Check for revenge trading
for i in range(1, len(trades)):
if trades[i-1].get('pnl', 0) < 0:
time_diff = (trades[i].get('entry_time', datetime.now()) -
trades[i-1].get('exit_time', datetime.now())).total_seconds() / 60
if time_diff < 15: # Trade within 15 minutes of loss
detected_biases.append({
'bias': TradingBias.REVENGE_TRADING,
'severity': 'medium',
'evidence': f"New trade within {time_diff:.0f} minutes of losing trade"
})
return detected_biases
def get_mitigation(self, bias: TradingBias) -> str:
"""Get mitigation strategy for a bias."""
indicator = self.bias_indicators.get(bias)
return indicator.mitigation if indicator else "No specific mitigation available"
# Demonstrate bias detection
detector = BiasDetector()
# Sample trades with potential biases
sample_trades = [
{'pnl': 50, 'duration_hours': 2, 'entry_time': datetime(2024,1,1,10,0), 'exit_time': datetime(2024,1,1,12,0)},
{'pnl': -100, 'duration_hours': 8, 'entry_time': datetime(2024,1,1,13,0), 'exit_time': datetime(2024,1,1,21,0)},
{'pnl': 30, 'duration_hours': 1, 'entry_time': datetime(2024,1,1,21,5), 'exit_time': datetime(2024,1,1,22,5)}, # Quick trade after loss
{'pnl': -80, 'duration_hours': 10, 'entry_time': datetime(2024,1,2,9,0), 'exit_time': datetime(2024,1,2,19,0)},
]
biases = detector.analyze_trades(sample_trades)
print("Detected Biases:")
print("-" * 60)
for b in biases:
print(f"\nBias: {b['bias'].value}")
print(f"Severity: {b['severity']}")
print(f"Evidence: {b['evidence']}")
print(f"Mitigation: {detector.get_mitigation(b['bias'])}")
class EmotionalStateTracker:
"""Track emotional state during trading."""
STATES = ['calm', 'focused', 'anxious', 'frustrated', 'euphoric', 'fearful']
def __init__(self):
self.entries: List[Dict] = []
def log_state(self, state: str, notes: str = "", context: Dict = None):
"""Log current emotional state."""
if state not in self.STATES:
raise ValueError(f"Unknown state. Use one of: {self.STATES}")
entry = {
'timestamp': datetime.now(),
'state': state,
'notes': notes,
'context': context or {}
}
self.entries.append(entry)
def analyze_state_patterns(self) -> Dict:
"""Analyze patterns in emotional states."""
if not self.entries:
return {}
state_counts = defaultdict(int)
for entry in self.entries:
state_counts[entry['state']] += 1
total = len(self.entries)
return {
'distribution': {s: count/total*100 for s, count in state_counts.items()},
'most_common': max(state_counts, key=state_counts.get),
'total_entries': total
}
def correlate_with_performance(self, trades: List[Dict]) -> Dict:
"""Correlate emotional states with trading performance."""
# Match trades with nearest emotional state entry
state_performance = defaultdict(list)
for trade in trades:
trade_time = trade.get('entry_time', datetime.now())
# Find nearest emotional state entry
nearest_state = None
min_diff = float('inf')
for entry in self.entries:
diff = abs((trade_time - entry['timestamp']).total_seconds())
if diff < min_diff:
min_diff = diff
nearest_state = entry['state']
if nearest_state and min_diff < 3600: # Within 1 hour
state_performance[nearest_state].append(trade.get('pnl', 0))
# Calculate average P&L by state
return {
state: {
'avg_pnl': np.mean(pnls),
'trade_count': len(pnls),
'win_rate': len([p for p in pnls if p > 0]) / len(pnls) * 100 if pnls else 0
}
for state, pnls in state_performance.items()
}
# Example usage
emotional_tracker = EmotionalStateTracker()
emotional_tracker.log_state('calm', 'Morning routine complete, ready to trade')
emotional_tracker.log_state('focused', 'Good setup developing on EURUSD')
emotional_tracker.log_state('anxious', 'Large position, uncertain about direction')
emotional_tracker.log_state('frustrated', 'Stopped out on clear manipulation')
analysis = emotional_tracker.analyze_state_patterns()
print("Emotional State Analysis:")
print(f"Most common state: {analysis.get('most_common', 'N/A')}")
print(f"Distribution: {analysis.get('distribution', {})}")
Section 14.2: Building a Trading Journal
A comprehensive trading journal is essential for improvement.
@dataclass
class JournalEntry:
"""Complete journal entry for a trade."""
# Trade identification
trade_id: str
timestamp: datetime
# Trade details
instrument: str
direction: str
entry_price: float
exit_price: float
position_size: float
# Risk management
stop_loss: float
take_profit: float
risk_reward: float
# Results
pnl: float
pnl_pips: float
duration_minutes: int
# Analysis
setup_type: str
timeframe: str
market_conditions: str
# Psychology
emotional_state: str
confidence_level: int # 1-10
followed_plan: bool
# Reflection
entry_reasoning: str = ""
exit_reasoning: str = ""
lessons_learned: str = ""
what_would_do_differently: str = ""
# Tags for filtering
tags: List[str] = field(default_factory=list)
# Screenshot paths
screenshots: List[str] = field(default_factory=list)
def to_dict(self) -> Dict:
return {
'trade_id': self.trade_id,
'timestamp': self.timestamp.isoformat(),
'instrument': self.instrument,
'direction': self.direction,
'entry_price': self.entry_price,
'exit_price': self.exit_price,
'position_size': self.position_size,
'stop_loss': self.stop_loss,
'take_profit': self.take_profit,
'risk_reward': self.risk_reward,
'pnl': self.pnl,
'pnl_pips': self.pnl_pips,
'duration_minutes': self.duration_minutes,
'setup_type': self.setup_type,
'timeframe': self.timeframe,
'market_conditions': self.market_conditions,
'emotional_state': self.emotional_state,
'confidence_level': self.confidence_level,
'followed_plan': self.followed_plan,
'entry_reasoning': self.entry_reasoning,
'exit_reasoning': self.exit_reasoning,
'lessons_learned': self.lessons_learned,
'what_would_do_differently': self.what_would_do_differently,
'tags': self.tags,
'screenshots': self.screenshots
}
class TradingJournal:
"""Comprehensive trading journal system."""
def __init__(self, journal_path: str = None):
self.entries: List[JournalEntry] = []
self.journal_path = journal_path
self.entry_count = 0
def add_entry(self, entry: JournalEntry):
"""Add a new journal entry."""
self.entries.append(entry)
self.entry_count += 1
def create_entry_from_trade(self, trade: Dict,
psychological_data: Dict = None,
analysis_data: Dict = None) -> JournalEntry:
"""Create a journal entry from trade data."""
self.entry_count += 1
psych = psychological_data or {}
analysis = analysis_data or {}
# Calculate risk/reward
entry = trade.get('entry_price', 0)
sl = trade.get('stop_loss', entry)
tp = trade.get('take_profit', entry)
risk = abs(entry - sl) if sl else 0
reward = abs(tp - entry) if tp else 0
rr = reward / risk if risk > 0 else 0
return JournalEntry(
trade_id=f"J{self.entry_count:06d}",
timestamp=trade.get('entry_time', datetime.now()),
instrument=trade.get('instrument', 'Unknown'),
direction=trade.get('direction', 'Unknown'),
entry_price=entry,
exit_price=trade.get('exit_price', 0),
position_size=trade.get('position_size', 0),
stop_loss=sl,
take_profit=tp,
risk_reward=rr,
pnl=trade.get('pnl', 0),
pnl_pips=trade.get('pnl_pips', 0),
duration_minutes=trade.get('duration_minutes', 0),
setup_type=analysis.get('setup_type', 'Unclassified'),
timeframe=analysis.get('timeframe', 'Unknown'),
market_conditions=analysis.get('market_conditions', 'Unknown'),
emotional_state=psych.get('emotional_state', 'Not recorded'),
confidence_level=psych.get('confidence', 5),
followed_plan=psych.get('followed_plan', True),
entry_reasoning=analysis.get('entry_reasoning', ''),
exit_reasoning=analysis.get('exit_reasoning', ''),
tags=analysis.get('tags', [])
)
def search(self, **filters) -> List[JournalEntry]:
"""Search journal entries with filters."""
results = self.entries
if 'instrument' in filters:
results = [e for e in results if e.instrument == filters['instrument']]
if 'direction' in filters:
results = [e for e in results if e.direction == filters['direction']]
if 'setup_type' in filters:
results = [e for e in results if e.setup_type == filters['setup_type']]
if 'profitable' in filters:
if filters['profitable']:
results = [e for e in results if e.pnl > 0]
else:
results = [e for e in results if e.pnl <= 0]
if 'tag' in filters:
results = [e for e in results if filters['tag'] in e.tags]
if 'date_from' in filters:
results = [e for e in results if e.timestamp >= filters['date_from']]
if 'date_to' in filters:
results = [e for e in results if e.timestamp <= filters['date_to']]
return results
def get_statistics(self, entries: List[JournalEntry] = None) -> Dict:
"""Calculate statistics for journal entries."""
if entries is None:
entries = self.entries
if not entries:
return {}
winners = [e for e in entries if e.pnl > 0]
losers = [e for e in entries if e.pnl <= 0]
return {
'total_trades': len(entries),
'win_rate': len(winners) / len(entries) * 100,
'total_pnl': sum(e.pnl for e in entries),
'avg_win': np.mean([e.pnl for e in winners]) if winners else 0,
'avg_loss': np.mean([e.pnl for e in losers]) if losers else 0,
'avg_rr_planned': np.mean([e.risk_reward for e in entries]),
'plan_adherence': len([e for e in entries if e.followed_plan]) / len(entries) * 100,
'avg_confidence': np.mean([e.confidence_level for e in entries]),
'top_setup': self._get_top_setup(entries),
'worst_setup': self._get_worst_setup(entries)
}
def _get_top_setup(self, entries: List[JournalEntry]) -> str:
"""Find best performing setup type."""
setup_pnl = defaultdict(list)
for e in entries:
setup_pnl[e.setup_type].append(e.pnl)
if not setup_pnl:
return "N/A"
avg_by_setup = {s: np.mean(pnls) for s, pnls in setup_pnl.items()}
return max(avg_by_setup, key=avg_by_setup.get)
def _get_worst_setup(self, entries: List[JournalEntry]) -> str:
"""Find worst performing setup type."""
setup_pnl = defaultdict(list)
for e in entries:
setup_pnl[e.setup_type].append(e.pnl)
if not setup_pnl:
return "N/A"
avg_by_setup = {s: np.mean(pnls) for s, pnls in setup_pnl.items()}
return min(avg_by_setup, key=avg_by_setup.get)
def save(self, path: str = None):
"""Save journal to file."""
path = path or self.journal_path
if not path:
raise ValueError("No path specified")
data = [e.to_dict() for e in self.entries]
with open(path, 'w') as f:
json.dump(data, f, indent=2)
def load(self, path: str = None):
"""Load journal from file."""
path = path or self.journal_path
if not path:
raise ValueError("No path specified")
with open(path, 'r') as f:
data = json.load(f)
# Convert back to JournalEntry objects
# (simplified - would need proper datetime parsing)
# Demonstrate trading journal
journal = TradingJournal()
# Add sample entries
sample_trades_for_journal = [
{
'instrument': 'EURUSD',
'direction': 'long',
'entry_price': 1.0850,
'exit_price': 1.0920,
'stop_loss': 1.0800,
'take_profit': 1.0950,
'position_size': 10000,
'pnl': 70,
'pnl_pips': 70,
'duration_minutes': 180,
'entry_time': datetime(2024, 1, 15, 10, 0)
},
{
'instrument': 'GBPUSD',
'direction': 'short',
'entry_price': 1.2700,
'exit_price': 1.2650,
'stop_loss': 1.2750,
'take_profit': 1.2600,
'position_size': 10000,
'pnl': 50,
'pnl_pips': 50,
'duration_minutes': 240,
'entry_time': datetime(2024, 1, 16, 14, 0)
},
{
'instrument': 'EURUSD',
'direction': 'long',
'entry_price': 1.0880,
'exit_price': 1.0850,
'stop_loss': 1.0850,
'take_profit': 1.0950,
'position_size': 10000,
'pnl': -30,
'pnl_pips': -30,
'duration_minutes': 60,
'entry_time': datetime(2024, 1, 17, 9, 0)
}
]
for trade in sample_trades_for_journal:
entry = journal.create_entry_from_trade(
trade,
psychological_data={'emotional_state': 'focused', 'confidence': 7, 'followed_plan': True},
analysis_data={'setup_type': 'Trend Continuation', 'timeframe': 'H1',
'market_conditions': 'Trending', 'tags': ['trend', 'momentum']}
)
journal.add_entry(entry)
# Get statistics
stats = journal.get_statistics()
print("Journal Statistics:")
print("-" * 40)
for key, value in stats.items():
if isinstance(value, float):
print(f"{key}: {value:.2f}")
else:
print(f"{key}: {value}")
Section 14.3: Performance Review
Systematic review processes for continuous improvement.
class WeeklyReview:
"""Generate weekly performance review."""
def __init__(self, journal: TradingJournal):
self.journal = journal
def generate(self, week_start: datetime, week_end: datetime = None) -> Dict:
"""Generate weekly review report."""
if week_end is None:
week_end = week_start + timedelta(days=7)
# Get trades for the week
week_entries = self.journal.search(date_from=week_start, date_to=week_end)
if not week_entries:
return {'week': week_start.strftime('%Y-%m-%d'), 'trades': 0}
stats = self.journal.get_statistics(week_entries)
# Analyze by day
daily_pnl = defaultdict(float)
for entry in week_entries:
day = entry.timestamp.strftime('%A')
daily_pnl[day] += entry.pnl
# Analyze by session
session_performance = self._analyze_by_session(week_entries)
# Find patterns
patterns = self._identify_patterns(week_entries)
return {
'week': week_start.strftime('%Y-%m-%d'),
'statistics': stats,
'daily_pnl': dict(daily_pnl),
'session_performance': session_performance,
'patterns': patterns,
'improvement_areas': self._identify_improvements(week_entries)
}
def _analyze_by_session(self, entries: List[JournalEntry]) -> Dict:
"""Analyze performance by trading session."""
sessions = {'Asian': [], 'London': [], 'New York': []}
for entry in entries:
hour = entry.timestamp.hour
if 0 <= hour < 8:
sessions['Asian'].append(entry.pnl)
elif 8 <= hour < 14:
sessions['London'].append(entry.pnl)
else:
sessions['New York'].append(entry.pnl)
return {
session: {
'trades': len(pnls),
'total_pnl': sum(pnls),
'avg_pnl': np.mean(pnls) if pnls else 0
}
for session, pnls in sessions.items()
}
def _identify_patterns(self, entries: List[JournalEntry]) -> List[str]:
"""Identify trading patterns."""
patterns = []
# Check win streaks
max_streak = 0
current_streak = 0
for entry in entries:
if entry.pnl > 0:
current_streak += 1
max_streak = max(max_streak, current_streak)
else:
current_streak = 0
if max_streak >= 3:
patterns.append(f"Winning streak of {max_streak} trades")
# Check for overtrading
trades_per_day = len(entries) / 5 # Assuming 5 trading days
if trades_per_day > 5:
patterns.append(f"High trading frequency: {trades_per_day:.1f} trades/day")
# Check plan adherence
adherence = len([e for e in entries if e.followed_plan]) / len(entries)
if adherence < 0.8:
patterns.append(f"Low plan adherence: {adherence*100:.0f}%")
return patterns
def _identify_improvements(self, entries: List[JournalEntry]) -> List[str]:
"""Identify areas for improvement."""
improvements = []
# Check if losing trades are too large
losers = [e for e in entries if e.pnl < 0]
winners = [e for e in entries if e.pnl > 0]
if losers and winners:
avg_loss = abs(np.mean([e.pnl for e in losers]))
avg_win = np.mean([e.pnl for e in winners])
if avg_loss > avg_win:
improvements.append("Cut losers faster - avg loss > avg win")
# Check emotional trading
emotional_losses = [e for e in entries if e.pnl < 0 and e.emotional_state in ['anxious', 'frustrated', 'euphoric']]
if emotional_losses:
improvements.append(f"{len(emotional_losses)} losing trades during emotional states")
return improvements
class MonthlyAnalysis:
"""Comprehensive monthly performance analysis."""
def __init__(self, journal: TradingJournal):
self.journal = journal
def generate(self, year: int, month: int) -> Dict:
"""Generate monthly analysis report."""
start = datetime(year, month, 1)
if month == 12:
end = datetime(year + 1, 1, 1)
else:
end = datetime(year, month + 1, 1)
entries = self.journal.search(date_from=start, date_to=end)
if not entries:
return {'month': f"{year}-{month:02d}", 'trades': 0}
return {
'month': f"{year}-{month:02d}",
'summary': self._get_summary(entries),
'by_instrument': self._analyze_by_instrument(entries),
'by_setup': self._analyze_by_setup(entries),
'equity_curve': self._build_equity_curve(entries),
'best_trade': self._get_best_trade(entries),
'worst_trade': self._get_worst_trade(entries),
'key_insights': self._generate_insights(entries)
}
def _get_summary(self, entries: List[JournalEntry]) -> Dict:
"""Get summary statistics."""
return self.journal.get_statistics(entries)
def _analyze_by_instrument(self, entries: List[JournalEntry]) -> Dict:
"""Analyze performance by instrument."""
by_instrument = defaultdict(list)
for e in entries:
by_instrument[e.instrument].append(e)
return {
inst: {
'trades': len(trades),
'total_pnl': sum(t.pnl for t in trades),
'win_rate': len([t for t in trades if t.pnl > 0]) / len(trades) * 100
}
for inst, trades in by_instrument.items()
}
def _analyze_by_setup(self, entries: List[JournalEntry]) -> Dict:
"""Analyze performance by setup type."""
by_setup = defaultdict(list)
for e in entries:
by_setup[e.setup_type].append(e)
return {
setup: {
'trades': len(trades),
'total_pnl': sum(t.pnl for t in trades),
'win_rate': len([t for t in trades if t.pnl > 0]) / len(trades) * 100,
'avg_rr': np.mean([t.risk_reward for t in trades])
}
for setup, trades in by_setup.items()
}
def _build_equity_curve(self, entries: List[JournalEntry]) -> List[Dict]:
"""Build equity curve data."""
sorted_entries = sorted(entries, key=lambda x: x.timestamp)
equity = 0
curve = []
for e in sorted_entries:
equity += e.pnl
curve.append({
'date': e.timestamp.strftime('%Y-%m-%d'),
'equity': equity
})
return curve
def _get_best_trade(self, entries: List[JournalEntry]) -> Dict:
"""Get best trade of the month."""
best = max(entries, key=lambda x: x.pnl)
return {
'instrument': best.instrument,
'pnl': best.pnl,
'setup': best.setup_type,
'date': best.timestamp.strftime('%Y-%m-%d')
}
def _get_worst_trade(self, entries: List[JournalEntry]) -> Dict:
"""Get worst trade of the month."""
worst = min(entries, key=lambda x: x.pnl)
return {
'instrument': worst.instrument,
'pnl': worst.pnl,
'setup': worst.setup_type,
'date': worst.timestamp.strftime('%Y-%m-%d')
}
def _generate_insights(self, entries: List[JournalEntry]) -> List[str]:
"""Generate actionable insights."""
insights = []
# Find best setup
by_setup = self._analyze_by_setup(entries)
if by_setup:
best_setup = max(by_setup.items(), key=lambda x: x[1]['total_pnl'])
insights.append(f"Best performing setup: {best_setup[0]} (${best_setup[1]['total_pnl']:.2f})")
# Check confidence correlation
high_conf = [e for e in entries if e.confidence_level >= 7]
low_conf = [e for e in entries if e.confidence_level < 7]
if high_conf and low_conf:
high_conf_wr = len([e for e in high_conf if e.pnl > 0]) / len(high_conf)
low_conf_wr = len([e for e in low_conf if e.pnl > 0]) / len(low_conf)
if high_conf_wr > low_conf_wr + 0.1:
insights.append(f"High confidence trades outperform ({high_conf_wr*100:.0f}% vs {low_conf_wr*100:.0f}% win rate)")
return insights
# Generate weekly review
weekly_review = WeeklyReview(journal)
review = weekly_review.generate(datetime(2024, 1, 15))
print("Weekly Review:")
print("=" * 50)
print(f"Week of: {review['week']}")
print(f"\nStatistics:")
for key, value in review.get('statistics', {}).items():
if isinstance(value, float):
print(f" {key}: {value:.2f}")
else:
print(f" {key}: {value}")
print(f"\nPatterns: {review.get('patterns', [])}")
print(f"Improvements: {review.get('improvement_areas', [])}")
Section 14.4: Continuous Improvement
Systematic frameworks for ongoing trading improvement.
@dataclass
class ImprovementGoal:
"""A specific improvement goal."""
id: str
description: str
metric: str
target_value: float
current_value: float
deadline: datetime
actions: List[str] = field(default_factory=list)
progress_notes: List[Dict] = field(default_factory=list)
@property
def progress_pct(self) -> float:
"""Calculate progress towards goal."""
if self.target_value == 0:
return 0
return (self.current_value / self.target_value) * 100
@property
def is_achieved(self) -> bool:
return self.current_value >= self.target_value
class ImprovementTracker:
"""Track and manage improvement goals."""
def __init__(self):
self.goals: Dict[str, ImprovementGoal] = {}
self.goal_count = 0
def add_goal(self, description: str, metric: str,
target_value: float, deadline: datetime,
current_value: float = 0, actions: List[str] = None) -> str:
"""Add a new improvement goal."""
self.goal_count += 1
goal_id = f"GOAL-{self.goal_count:03d}"
self.goals[goal_id] = ImprovementGoal(
id=goal_id,
description=description,
metric=metric,
target_value=target_value,
current_value=current_value,
deadline=deadline,
actions=actions or []
)
return goal_id
def update_progress(self, goal_id: str, new_value: float, note: str = ""):
"""Update progress on a goal."""
if goal_id not in self.goals:
return
goal = self.goals[goal_id]
goal.current_value = new_value
goal.progress_notes.append({
'date': datetime.now(),
'value': new_value,
'note': note
})
def get_active_goals(self) -> List[ImprovementGoal]:
"""Get goals that are not yet achieved."""
return [g for g in self.goals.values() if not g.is_achieved]
def get_status_report(self) -> str:
"""Generate status report for all goals."""
lines = [
"\n" + "=" * 60,
"IMPROVEMENT GOALS STATUS",
"=" * 60,
""
]
for goal in self.goals.values():
status = "ACHIEVED" if goal.is_achieved else "IN PROGRESS"
lines.append(f"[{goal.id}] {goal.description}")
lines.append(f" Metric: {goal.metric}")
lines.append(f" Progress: {goal.current_value:.2f} / {goal.target_value:.2f} ({goal.progress_pct:.0f}%)")
lines.append(f" Deadline: {goal.deadline.strftime('%Y-%m-%d')}")
lines.append(f" Status: {status}")
lines.append("")
return "\n".join(lines)
class ABTestFramework:
"""A/B testing framework for trading strategies."""
def __init__(self):
self.tests: Dict[str, Dict] = {}
self.test_count = 0
def create_test(self, name: str, description: str,
variant_a: str, variant_b: str,
sample_size: int = 50) -> str:
"""Create a new A/B test."""
self.test_count += 1
test_id = f"TEST-{self.test_count:03d}"
self.tests[test_id] = {
'name': name,
'description': description,
'variant_a': variant_a,
'variant_b': variant_b,
'sample_size': sample_size,
'results_a': [],
'results_b': [],
'status': 'active',
'created': datetime.now()
}
return test_id
def record_result(self, test_id: str, variant: str, pnl: float):
"""Record a result for a test variant."""
if test_id not in self.tests:
return
test = self.tests[test_id]
if variant == 'A':
test['results_a'].append(pnl)
else:
test['results_b'].append(pnl)
# Check if test is complete
if (len(test['results_a']) >= test['sample_size'] and
len(test['results_b']) >= test['sample_size']):
test['status'] = 'complete'
def analyze_test(self, test_id: str) -> Dict:
"""Analyze test results."""
if test_id not in self.tests:
return {}
test = self.tests[test_id]
results_a = test['results_a']
results_b = test['results_b']
if not results_a or not results_b:
return {'status': 'insufficient_data'}
# Calculate statistics
mean_a = np.mean(results_a)
mean_b = np.mean(results_b)
# Simple t-test
from scipy import stats
t_stat, p_value = stats.ttest_ind(results_a, results_b)
winner = 'A' if mean_a > mean_b else 'B'
significant = p_value < 0.05
return {
'test_name': test['name'],
'variant_a': {
'description': test['variant_a'],
'samples': len(results_a),
'mean_pnl': mean_a,
'win_rate': len([r for r in results_a if r > 0]) / len(results_a) * 100
},
'variant_b': {
'description': test['variant_b'],
'samples': len(results_b),
'mean_pnl': mean_b,
'win_rate': len([r for r in results_b if r > 0]) / len(results_b) * 100
},
'p_value': p_value,
'statistically_significant': significant,
'recommended': winner if significant else 'No clear winner'
}
# Demonstrate improvement tracking
improvement_tracker = ImprovementTracker()
# Add improvement goals
improvement_tracker.add_goal(
description="Improve win rate",
metric="win_rate_pct",
target_value=55,
current_value=48,
deadline=datetime(2024, 3, 1),
actions=["Only take A+ setups", "Wait for confirmation"]
)
improvement_tracker.add_goal(
description="Reduce average loss size",
metric="avg_loss_usd",
target_value=30,
current_value=45,
deadline=datetime(2024, 3, 1),
actions=["Use tighter stops", "Cut losers faster"]
)
improvement_tracker.add_goal(
description="Increase plan adherence",
metric="plan_adherence_pct",
target_value=90,
current_value=75,
deadline=datetime(2024, 2, 15),
actions=["Pre-define entries/exits", "Use checklist"]
)
print(improvement_tracker.get_status_report())
# Demonstrate A/B testing
ab_framework = ABTestFramework()
# Create a test
test_id = ab_framework.create_test(
name="Stop Loss Placement",
description="Testing ATR-based stops vs fixed pip stops",
variant_a="ATR-based stop (2x ATR)",
variant_b="Fixed 30 pip stop",
sample_size=20
)
# Simulate results
np.random.seed(42)
# Variant A results (ATR-based)
for _ in range(25):
pnl = np.random.normal(15, 40) # Slightly positive expectancy
ab_framework.record_result(test_id, 'A', pnl)
# Variant B results (Fixed stop)
for _ in range(25):
pnl = np.random.normal(5, 35) # Lower expectancy
ab_framework.record_result(test_id, 'B', pnl)
# Analyze
analysis = ab_framework.analyze_test(test_id)
print("A/B Test Results:")
print("=" * 50)
print(f"Test: {analysis['test_name']}")
print(f"\nVariant A ({analysis['variant_a']['description']}):")
print(f" Samples: {analysis['variant_a']['samples']}")
print(f" Mean P&L: ${analysis['variant_a']['mean_pnl']:.2f}")
print(f" Win Rate: {analysis['variant_a']['win_rate']:.1f}%")
print(f"\nVariant B ({analysis['variant_b']['description']}):")
print(f" Samples: {analysis['variant_b']['samples']}")
print(f" Mean P&L: ${analysis['variant_b']['mean_pnl']:.2f}")
print(f" Win Rate: {analysis['variant_b']['win_rate']:.1f}%")
print(f"\nP-value: {analysis['p_value']:.4f}")
print(f"Statistically Significant: {analysis['statistically_significant']}")
print(f"Recommendation: {analysis['recommended']}")
Exercises
Exercise 1: Bias Detection Enhancement (Guided)
Enhance the bias detector with additional detection rules.
class EnhancedBiasDetector(BiasDetector):
"""Enhanced bias detection with more rules."""
def detect_overtrading(self, trades: List[Dict], threshold: int = 10) -> Optional[Dict]:
"""Detect overtrading patterns."""
if not trades:
return None
# Group trades by date
trades_by_date = defaultdict(list)
for t in trades:
date = t.get('entry_time', datetime.now()).date()
trades_by_date[date].append(t)
# Find days with excessive trading
overtrading_days = [d for d, ts in trades_by_date.items() if len(ts) > ______] # Check threshold
if overtrading_days:
return {
'bias': 'overtrading',
'severity': 'high' if len(overtrading_days) > 3 else 'medium',
'evidence': f"{len(overtrading_days)} days with >{threshold} trades"
}
return None
def detect_anchoring(self, trades: List[Dict]) -> Optional[Dict]:
"""Detect anchoring to specific price levels."""
# Check if many stops/targets are at round numbers
round_number_count = 0
total = 0
for t in trades:
sl = t.get('stop_loss', 0)
tp = t.get('take_profit', 0)
for price in [sl, tp]:
if price > 0:
total += 1
# Check if price is a round number (ends in 00)
if int(price * 10000) % 100 == 0:
round_number_count += ______ # Increment count
if total > 0 and round_number_count / total > 0.5:
return {
'bias': TradingBias.ANCHORING,
'severity': 'medium',
'evidence': f"{round_number_count/total*100:.0f}% of stops/targets at round numbers"
}
return None
def analyze_all(self, trades: List[Dict]) -> List[Dict]:
"""Run all bias detection methods."""
biases = self.analyze_trades(trades)
overtrading = self.detect_overtrading(trades)
if overtrading:
biases.______(overtrading) # Add to list
anchoring = self.detect_anchoring(trades)
if anchoring:
biases.append(anchoring)
return biases
# Test enhanced detector
enhanced_detector = EnhancedBiasDetector()
biases = enhanced_detector.analyze_all(sample_trades)
print(f"Detected {len(biases)} potential biases")
Exercise 2: Journal Entry Template (Guided)
Create a journal entry template generator.
class JournalTemplateGenerator:
"""Generate journal entry templates."""
def __init__(self):
self.prompts = {
'entry_reasoning': [
"What setup triggered this trade?",
"What was the market structure?",
"What was your edge in this trade?"
],
'exit_reasoning': [
"Why did you exit at this point?",
"Was this a planned or discretionary exit?",
"Did price action confirm your exit?"
],
'lessons': [
"What would you do differently?",
"What did this trade teach you?",
"How can you improve similar setups?"
]
}
def generate_pre_trade(self, trade_details: Dict) -> str:
"""Generate pre-trade checklist."""
lines = [
"=" * 50,
"PRE-TRADE CHECKLIST",
"=" * 50,
"",
f"Instrument: {trade_details.get('instrument', '_________')}",
f"Direction: {trade_details.get('direction', '_________')}",
"",
"SETUP CONFIRMATION:",
"[ ] Is this my A+ setup?",
"[ ] Is the trend aligned with my direction?",
"[ ] Is there clear support/resistance?",
"",
"RISK CHECK:",
"[ ] Position size within risk limits?",
"[ ] Stop loss clearly defined?",
"[ ] Risk:Reward >= 1:2?",
"",
"EMOTIONAL CHECK:",
"Current emotional state: __________",
"Confidence (1-10): __________",
"[ ] Not revenge trading",
"[ ] Not FOMO"
]
return "\n".______(lines) # Join lines with newline
def generate_post_trade(self, trade_result: Dict) -> str:
"""Generate post-trade review template."""
outcome = "WIN" if trade_result.get('pnl', 0) > 0 else "______" # Set outcome
lines = [
"=" * 50,
f"POST-TRADE REVIEW - {outcome}",
"=" * 50,
"",
f"P&L: ${trade_result.get('pnl', 0):.2f}",
f"Pips: {trade_result.get('pnl_pips', 0):.1f}",
"",
"EXECUTION REVIEW:",
]
for prompt in self.prompts['entry_reasoning']:
lines.append(f"Q: {prompt}")
lines.append("A: _________________________________")
lines.append("")
lines.append("\nLESSONS LEARNED:")
for prompt in self.prompts['lessons']:
lines.append(f"Q: {prompt}")
lines.append("A: _________________________________")
lines.append("")
return "\n".join(______)
# Test template generator
template_gen = JournalTemplateGenerator()
print(template_gen.generate_pre_trade({'instrument': 'EURUSD', 'direction': 'long'}))
Exercise 3: Performance Heatmap (Guided)
Create a performance heatmap generator.
class PerformanceHeatmap:
"""Generate performance heatmaps."""
def __init__(self, journal: TradingJournal):
self.journal = journal
def by_day_hour(self) -> pd.DataFrame:
"""Create heatmap data by day of week and hour."""
# Initialize matrix
days = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri']
hours = list(range(0, 24))
data = pd.DataFrame(0.0, index=days, columns=hours)
counts = pd.DataFrame(0, index=days, columns=hours)
for entry in self.journal.entries:
day = entry.timestamp.strftime('%a')
hour = entry.timestamp.______ # Get hour from timestamp
if day in days:
data.loc[day, hour] += entry.pnl
counts.loc[day, hour] += 1
# Calculate average (avoid division by zero)
avg_data = data.div(counts.replace(0, 1))
return avg_data
def plot(self, data: pd.DataFrame, title: str):
"""Plot heatmap."""
plt.figure(figsize=(14, 5))
plt.imshow(data.values, cmap='RdYlGn', aspect='auto')
plt.colorbar(label='Avg P&L')
plt.yticks(range(len(data.index)), data.______)
plt.xticks(range(len(data.columns)), data.columns)
plt.xlabel('Hour (UTC)')
plt.ylabel('Day of Week')
plt.title(title)
plt.tight_layout()
plt.show()
# Create heatmap
heatmap = PerformanceHeatmap(journal)
data = heatmap.by_day_hour()
print("Performance by Day/Hour:")
print(data.head())
Exercise 4: Emotional Journal Integration
Build a system that integrates emotional tracking with trade journaling.
# Exercise 4: Build an EmotionalJournalIntegration class that:
# 1. Links emotional state entries to specific trades
# 2. Calculates performance by emotional state
# 3. Identifies emotional patterns that lead to losses
# 4. Generates recommendations based on emotional patterns
# 5. Creates a report showing emotional impact on trading
# Your code here
Exercise 5: Strategy Evolution Tracker
Track how your trading strategy evolves over time.
# Exercise 5: Build a StrategyEvolutionTracker class that:
# 1. Records strategy changes with timestamps and reasons
# 2. Compares performance before/after changes
# 3. Identifies which changes improved performance
# 4. Tracks rule additions/modifications/removals
# 5. Generates an evolution timeline
# Your code here
Exercise 6: Automated Review Generator
Build a system that automatically generates comprehensive reviews.
# Exercise 6: Build an AutomatedReviewGenerator class that:
# 1. Generates daily, weekly, and monthly reviews automatically
# 2. Compares current period to historical averages
# 3. Highlights anomalies and unusual patterns
# 4. Creates actionable recommendations
# 5. Exports reviews to multiple formats (text, HTML, PDF)
# Your code here
Module Project: Automated Trading Journal
Build a complete automated trading journal system.
class AutomatedTradingJournal:
"""
Complete automated trading journal system.
Integrates:
- Trade logging
- Psychological tracking
- Bias detection
- Performance analysis
- Improvement tracking
- Automated reviews
"""
def __init__(self):
self.journal = TradingJournal()
self.emotional_tracker = EmotionalStateTracker()
self.bias_detector = BiasDetector()
self.improvement_tracker = ImprovementTracker()
self.ab_framework = ABTestFramework()
def log_trade(self, trade: Dict, emotional_state: str = None,
analysis: Dict = None):
"""Log a trade with full context."""
# Log emotional state if provided
if emotional_state:
self.emotional_tracker.log_state(
emotional_state,
context={'trade_entry': True}
)
# Create journal entry
psych_data = {
'emotional_state': emotional_state or 'not_recorded',
'confidence': analysis.get('confidence', 5) if analysis else 5,
'followed_plan': analysis.get('followed_plan', True) if analysis else True
}
entry = self.journal.create_entry_from_trade(trade, psych_data, analysis)
self.journal.add_entry(entry)
return entry
def run_bias_check(self, lookback_days: int = 30) -> List[Dict]:
"""Run bias detection on recent trades."""
cutoff = datetime.now() - timedelta(days=lookback_days)
recent_entries = self.journal.search(date_from=cutoff)
trades = [
{
'pnl': e.pnl,
'duration_hours': e.duration_minutes / 60,
'entry_time': e.timestamp,
'exit_time': e.timestamp + timedelta(minutes=e.duration_minutes)
}
for e in recent_entries
]
return self.bias_detector.analyze_trades(trades)
def generate_weekly_review(self) -> Dict:
"""Generate weekly performance review."""
week_start = datetime.now() - timedelta(days=7)
weekly_review = WeeklyReview(self.journal)
return weekly_review.generate(week_start)
def generate_monthly_analysis(self) -> Dict:
"""Generate monthly analysis."""
now = datetime.now()
monthly = MonthlyAnalysis(self.journal)
return monthly.generate(now.year, now.month)
def get_improvement_suggestions(self) -> List[str]:
"""Get personalized improvement suggestions."""
suggestions = []
stats = self.journal.get_statistics()
# Check win rate
if stats.get('win_rate', 0) < 50:
suggestions.append("Focus on improving trade selection - win rate below 50%")
# Check plan adherence
if stats.get('plan_adherence', 100) < 80:
suggestions.append("Improve discipline - plan adherence below 80%")
# Check emotional correlation
emotional_analysis = self.emotional_tracker.analyze_state_patterns()
if emotional_analysis.get('most_common') in ['anxious', 'frustrated']:
suggestions.append("Address emotional management - frequently trading in negative states")
# Add bias-specific suggestions
biases = self.run_bias_check()
for bias in biases:
suggestions.append(f"Address {bias['bias'].value}: {self.bias_detector.get_mitigation(bias['bias'])}")
return suggestions
def generate_comprehensive_report(self) -> str:
"""Generate comprehensive trading report."""
stats = self.journal.get_statistics()
biases = self.run_bias_check()
suggestions = self.get_improvement_suggestions()
lines = [
"\n" + "=" * 70,
"COMPREHENSIVE TRADING REPORT",
f"Generated: {datetime.now().strftime('%Y-%m-%d %H:%M')}",
"=" * 70,
"",
"PERFORMANCE SUMMARY",
"-" * 40,
]
for key, value in stats.items():
if isinstance(value, float):
lines.append(f" {key}: {value:.2f}")
else:
lines.append(f" {key}: {value}")
lines.append("")
lines.append("DETECTED BIASES")
lines.append("-" * 40)
if biases:
for bias in biases:
lines.append(f" [{bias['severity'].upper()}] {bias['bias'].value}")
lines.append(f" Evidence: {bias['evidence']}")
else:
lines.append(" No significant biases detected")
lines.append("")
lines.append("IMPROVEMENT SUGGESTIONS")
lines.append("-" * 40)
for i, suggestion in enumerate(suggestions, 1):
lines.append(f" {i}. {suggestion}")
lines.append("")
lines.append("=" * 70)
return "\n".join(lines)
# Demonstrate the automated trading journal
auto_journal = AutomatedTradingJournal()
# Log some trades
for trade in sample_trades_for_journal:
auto_journal.log_trade(
trade,
emotional_state='focused',
analysis={
'setup_type': 'Trend Continuation',
'timeframe': 'H1',
'market_conditions': 'Trending',
'confidence': 7,
'followed_plan': True
}
)
# Generate comprehensive report
report = auto_journal.generate_comprehensive_report()
print(report)
Key Takeaways
-
Trading Psychology: Understanding and mitigating biases is as important as technical skills
-
Journaling: Detailed trade documentation enables systematic improvement
-
Regular Reviews: Weekly and monthly reviews identify patterns and areas for improvement
-
Continuous Improvement: A/B testing and goal tracking accelerate development
-
Automation: Automated systems ensure consistency and reduce manual effort
Next: Capstone Project - Build a complete 24-hour Forex/Futures Trading System
Capstone Project: 24-Hour Forex/Futures Trading System
| Duration | ~8-10 hours |
| Skill Level | Advanced |
| Prerequisites | All Course 5 Modules |
Project Overview
Build a complete, production-ready 24-hour trading system that combines everything you've learned in this course.
System Requirements
- Multi-currency monitoring
- Economic calendar integration
- Technical + fundamental signals
- Leveraged risk management
- OANDA or MT5 execution (simulated)
- 24-hour scheduling
- Performance tracking
- Automated journaling
Learning Objectives
By completing this capstone, you will demonstrate mastery of: - Forex and futures market mechanics - Technical and fundamental analysis integration - Risk management for leveraged products - Automated trading system development - Performance monitoring and analysis - Professional software engineering practices
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from datetime import datetime, timedelta
from typing import Dict, List, Optional, Tuple, Callable
from dataclasses import dataclass, field
from enum import Enum
from collections import defaultdict
import json
import logging
from abc import ABC, abstractmethod
Part 1: Core Infrastructure
Build the foundational components of the trading system.
1.1 Data Models
class OrderType(Enum):
MARKET = "market"
LIMIT = "limit"
STOP = "stop"
class OrderSide(Enum):
BUY = "buy"
SELL = "sell"
class TradingSession(Enum):
SYDNEY = "Sydney"
TOKYO = "Tokyo"
LONDON = "London"
NEW_YORK = "New_York"
@dataclass
class Tick:
"""Single price tick."""
instrument: str
bid: float
ask: float
timestamp: datetime
@property
def mid(self) -> float:
return (self.bid + self.ask) / 2
@property
def spread_pips(self) -> float:
return (self.ask - self.bid) * 10000
@dataclass
class OHLC:
"""OHLC candle data."""
instrument: str
timeframe: str
timestamp: datetime
open: float
high: float
low: float
close: float
volume: int = 0
@dataclass
class Order:
"""Trading order."""
order_id: str
instrument: str
side: OrderSide
order_type: OrderType
units: float
price: Optional[float] = None
stop_loss: Optional[float] = None
take_profit: Optional[float] = None
trailing_stop: Optional[float] = None
status: str = "pending"
fill_price: Optional[float] = None
fill_time: Optional[datetime] = None
@dataclass
class Position:
"""Open position."""
position_id: str
instrument: str
units: float
entry_price: float
entry_time: datetime
stop_loss: Optional[float] = None
take_profit: Optional[float] = None
current_price: float = 0.0
@property
def side(self) -> str:
return "long" if self.units > 0 else "short"
@property
def unrealized_pnl(self) -> float:
if self.units > 0:
return (self.current_price - self.entry_price) * self.units
else:
return (self.entry_price - self.current_price) * abs(self.units)
@property
def unrealized_pips(self) -> float:
diff = self.current_price - self.entry_price
if self.units < 0:
diff = -diff
return diff * 10000
@dataclass
class TradeRecord:
"""Completed trade record."""
trade_id: str
instrument: str
side: str
units: float
entry_price: float
exit_price: float
entry_time: datetime
exit_time: datetime
pnl: float
pnl_pips: float
setup_type: str = ""
notes: str = ""
1.2 Market Data Manager
class MarketDataManager:
"""Manage market data for multiple instruments."""
def __init__(self):
self.ticks: Dict[str, List[Tick]] = defaultdict(list)
self.candles: Dict[str, Dict[str, List[OHLC]]] = defaultdict(lambda: defaultdict(list))
self.current_prices: Dict[str, Tick] = {}
def update_tick(self, tick: Tick):
"""Update with new tick."""
self.ticks[tick.instrument].append(tick)
self.current_prices[tick.instrument] = tick
# Keep only last 10000 ticks per instrument
if len(self.ticks[tick.instrument]) > 10000:
self.ticks[tick.instrument] = self.ticks[tick.instrument][-10000:]
def add_candle(self, candle: OHLC):
"""Add OHLC candle."""
self.candles[candle.instrument][candle.timeframe].append(candle)
def get_price(self, instrument: str) -> Optional[Tick]:
"""Get current price for instrument."""
return self.current_prices.get(instrument)
def get_candles(self, instrument: str, timeframe: str, count: int = 100) -> List[OHLC]:
"""Get recent candles."""
candles = self.candles[instrument][timeframe]
return candles[-count:] if candles else []
def get_ohlc_dataframe(self, instrument: str, timeframe: str) -> pd.DataFrame:
"""Get candles as DataFrame."""
candles = self.get_candles(instrument, timeframe)
if not candles:
return pd.DataFrame()
data = [{
'timestamp': c.timestamp,
'open': c.open,
'high': c.high,
'low': c.low,
'close': c.close,
'volume': c.volume
} for c in candles]
df = pd.DataFrame(data)
df.set_index('timestamp', inplace=True)
return df
1.3 Simulated Broker
class SimulatedBroker:
"""Simulated broker for paper trading."""
def __init__(self, initial_balance: float = 10000.0, leverage: int = 50):
self.balance = initial_balance
self.initial_balance = initial_balance
self.leverage = leverage
self.positions: Dict[str, Position] = {}
self.orders: Dict[str, Order] = {}
self.trade_history: List[TradeRecord] = []
self.order_count = 0
self.position_count = 0
self.trade_count = 0
# Commission structure
self.spread_markup = 0.0001 # 1 pip additional spread
self.commission_per_lot = 0 # No commission (spread only)
def get_account_summary(self) -> Dict:
"""Get account summary."""
unrealized_pnl = sum(p.unrealized_pnl for p in self.positions.values())
equity = self.balance + unrealized_pnl
# Calculate margin used
margin_used = 0
for pos in self.positions.values():
notional = abs(pos.units) * pos.current_price
margin_used += notional / self.leverage
margin_available = equity - margin_used
margin_level = (equity / margin_used * 100) if margin_used > 0 else float('inf')
return {
'balance': self.balance,
'unrealized_pnl': unrealized_pnl,
'equity': equity,
'margin_used': margin_used,
'margin_available': margin_available,
'margin_level': margin_level,
'open_positions': len(self.positions),
'total_trades': len(self.trade_history)
}
def submit_order(self, instrument: str, side: OrderSide, units: float,
order_type: OrderType = OrderType.MARKET,
price: float = None, stop_loss: float = None,
take_profit: float = None, current_tick: Tick = None) -> Order:
"""Submit a new order."""
self.order_count += 1
order_id = f"ORD-{self.order_count:06d}"
order = Order(
order_id=order_id,
instrument=instrument,
side=side,
order_type=order_type,
units=units,
price=price,
stop_loss=stop_loss,
take_profit=take_profit
)
# For market orders, execute immediately
if order_type == OrderType.MARKET and current_tick:
self._execute_order(order, current_tick)
else:
self.orders[order_id] = order
return order
def _execute_order(self, order: Order, tick: Tick):
"""Execute an order at current price."""
# Determine fill price (include spread)
if order.side == OrderSide.BUY:
fill_price = tick.ask + self.spread_markup
else:
fill_price = tick.bid - self.spread_markup
order.fill_price = fill_price
order.fill_time = tick.timestamp
order.status = "filled"
# Check if this closes an existing position
if order.instrument in self.positions:
self._update_position(order, tick)
else:
self._open_position(order)
def _open_position(self, order: Order):
"""Open a new position."""
self.position_count += 1
position_id = f"POS-{self.position_count:06d}"
units = order.units if order.side == OrderSide.BUY else -order.units
position = Position(
position_id=position_id,
instrument=order.instrument,
units=units,
entry_price=order.fill_price,
entry_time=order.fill_time,
stop_loss=order.stop_loss,
take_profit=order.take_profit,
current_price=order.fill_price
)
self.positions[order.instrument] = position
def _update_position(self, order: Order, tick: Tick):
"""Update existing position (add to or close)."""
position = self.positions[order.instrument]
order_units = order.units if order.side == OrderSide.BUY else -order.units
new_units = position.units + order_units
if new_units == 0:
# Position fully closed
self._close_position(order.instrument, order.fill_price, tick.timestamp)
elif (position.units > 0 and new_units < 0) or (position.units < 0 and new_units > 0):
# Position reversed
self._close_position(order.instrument, order.fill_price, tick.timestamp)
# Open new position with remaining units
order.units = abs(new_units)
self._open_position(order)
else:
# Position increased
total_cost = position.entry_price * abs(position.units) + order.fill_price * order.units
position.units = new_units
position.entry_price = total_cost / abs(new_units)
def _close_position(self, instrument: str, exit_price: float, exit_time: datetime):
"""Close a position and record the trade."""
if instrument not in self.positions:
return
position = self.positions[instrument]
# Calculate P&L
if position.units > 0:
pnl = (exit_price - position.entry_price) * position.units
pnl_pips = (exit_price - position.entry_price) * 10000
else:
pnl = (position.entry_price - exit_price) * abs(position.units)
pnl_pips = (position.entry_price - exit_price) * 10000
# Update balance
self.balance += pnl
# Record trade
self.trade_count += 1
trade = TradeRecord(
trade_id=f"TRD-{self.trade_count:06d}",
instrument=instrument,
side=position.side,
units=abs(position.units),
entry_price=position.entry_price,
exit_price=exit_price,
entry_time=position.entry_time,
exit_time=exit_time,
pnl=pnl,
pnl_pips=pnl_pips
)
self.trade_history.append(trade)
# Remove position
del self.positions[instrument]
def update_positions(self, market_data: MarketDataManager):
"""Update position prices and check stops/targets."""
for instrument, position in list(self.positions.items()):
tick = market_data.get_price(instrument)
if tick:
position.current_price = tick.mid
# Check stop loss
if position.stop_loss:
if position.units > 0 and tick.bid <= position.stop_loss:
self._close_position(instrument, tick.bid, tick.timestamp)
elif position.units < 0 and tick.ask >= position.stop_loss:
self._close_position(instrument, tick.ask, tick.timestamp)
# Check take profit
if position.take_profit:
if position.units > 0 and tick.bid >= position.take_profit:
self._close_position(instrument, tick.bid, tick.timestamp)
elif position.units < 0 and tick.ask <= position.take_profit:
self._close_position(instrument, tick.ask, tick.timestamp)
def close_all_positions(self, market_data: MarketDataManager):
"""Close all open positions."""
for instrument in list(self.positions.keys()):
tick = market_data.get_price(instrument)
if tick:
position = self.positions[instrument]
exit_price = tick.bid if position.units > 0 else tick.ask
self._close_position(instrument, exit_price, tick.timestamp)
Part 2: Signal Generation
Build signal generation from technical and fundamental analysis.
2.1 Technical Indicators
class TechnicalIndicators:
"""Calculate technical indicators."""
@staticmethod
def sma(prices: pd.Series, period: int) -> pd.Series:
"""Simple Moving Average."""
return prices.rolling(window=period).mean()
@staticmethod
def ema(prices: pd.Series, period: int) -> pd.Series:
"""Exponential Moving Average."""
return prices.ewm(span=period, adjust=False).mean()
@staticmethod
def rsi(prices: pd.Series, period: int = 14) -> pd.Series:
"""Relative Strength Index."""
delta = prices.diff()
gain = (delta.where(delta > 0, 0)).rolling(window=period).mean()
loss = (-delta.where(delta < 0, 0)).rolling(window=period).mean()
rs = gain / loss
return 100 - (100 / (1 + rs))
@staticmethod
def atr(high: pd.Series, low: pd.Series, close: pd.Series, period: int = 14) -> pd.Series:
"""Average True Range."""
tr1 = high - low
tr2 = abs(high - close.shift())
tr3 = abs(low - close.shift())
tr = pd.concat([tr1, tr2, tr3], axis=1).max(axis=1)
return tr.rolling(window=period).mean()
@staticmethod
def bollinger_bands(prices: pd.Series, period: int = 20, std_dev: float = 2.0) -> Tuple[pd.Series, pd.Series, pd.Series]:
"""Bollinger Bands."""
middle = prices.rolling(window=period).mean()
std = prices.rolling(window=period).std()
upper = middle + std_dev * std
lower = middle - std_dev * std
return upper, middle, lower
@staticmethod
def macd(prices: pd.Series, fast: int = 12, slow: int = 26, signal: int = 9) -> Tuple[pd.Series, pd.Series, pd.Series]:
"""MACD indicator."""
ema_fast = prices.ewm(span=fast, adjust=False).mean()
ema_slow = prices.ewm(span=slow, adjust=False).mean()
macd_line = ema_fast - ema_slow
signal_line = macd_line.ewm(span=signal, adjust=False).mean()
histogram = macd_line - signal_line
return macd_line, signal_line, histogram
2.2 Signal Generator
@dataclass
class Signal:
"""Trading signal."""
instrument: str
direction: str # 'long' or 'short'
strength: float # 0.0 to 1.0
source: str # 'technical', 'fundamental', 'combined'
timestamp: datetime
entry_price: Optional[float] = None
stop_loss: Optional[float] = None
take_profit: Optional[float] = None
metadata: Dict = field(default_factory=dict)
class SignalGenerator(ABC):
"""Base class for signal generators."""
@abstractmethod
def generate(self, df: pd.DataFrame, instrument: str) -> Optional[Signal]:
"""Generate trading signal from data."""
pass
class TrendFollowingSignal(SignalGenerator):
"""Trend following signal using moving averages."""
def __init__(self, fast_period: int = 20, slow_period: int = 50, atr_period: int = 14):
self.fast_period = fast_period
self.slow_period = slow_period
self.atr_period = atr_period
def generate(self, df: pd.DataFrame, instrument: str) -> Optional[Signal]:
if len(df) < self.slow_period + 1:
return None
# Calculate indicators
fast_ma = TechnicalIndicators.ema(df['close'], self.fast_period)
slow_ma = TechnicalIndicators.ema(df['close'], self.slow_period)
atr = TechnicalIndicators.atr(df['high'], df['low'], df['close'], self.atr_period)
current_fast = fast_ma.iloc[-1]
current_slow = slow_ma.iloc[-1]
prev_fast = fast_ma.iloc[-2]
prev_slow = slow_ma.iloc[-2]
current_atr = atr.iloc[-1]
current_price = df['close'].iloc[-1]
# Check for crossover
signal = None
if prev_fast <= prev_slow and current_fast > current_slow:
# Bullish crossover
signal = Signal(
instrument=instrument,
direction='long',
strength=0.7,
source='technical',
timestamp=df.index[-1],
entry_price=current_price,
stop_loss=current_price - 2 * current_atr,
take_profit=current_price + 3 * current_atr,
metadata={'fast_ma': current_fast, 'slow_ma': current_slow, 'atr': current_atr}
)
elif prev_fast >= prev_slow and current_fast < current_slow:
# Bearish crossover
signal = Signal(
instrument=instrument,
direction='short',
strength=0.7,
source='technical',
timestamp=df.index[-1],
entry_price=current_price,
stop_loss=current_price + 2 * current_atr,
take_profit=current_price - 3 * current_atr,
metadata={'fast_ma': current_fast, 'slow_ma': current_slow, 'atr': current_atr}
)
return signal
class MeanReversionSignal(SignalGenerator):
"""Mean reversion signal using Bollinger Bands."""
def __init__(self, period: int = 20, std_dev: float = 2.0, rsi_period: int = 14):
self.period = period
self.std_dev = std_dev
self.rsi_period = rsi_period
def generate(self, df: pd.DataFrame, instrument: str) -> Optional[Signal]:
if len(df) < self.period + 1:
return None
# Calculate indicators
upper, middle, lower = TechnicalIndicators.bollinger_bands(df['close'], self.period, self.std_dev)
rsi = TechnicalIndicators.rsi(df['close'], self.rsi_period)
atr = TechnicalIndicators.atr(df['high'], df['low'], df['close'], 14)
current_price = df['close'].iloc[-1]
current_rsi = rsi.iloc[-1]
current_atr = atr.iloc[-1]
current_upper = upper.iloc[-1]
current_lower = lower.iloc[-1]
current_middle = middle.iloc[-1]
signal = None
# Oversold at lower band
if current_price <= current_lower and current_rsi < 30:
signal = Signal(
instrument=instrument,
direction='long',
strength=0.6 + (30 - current_rsi) / 100,
source='technical',
timestamp=df.index[-1],
entry_price=current_price,
stop_loss=current_price - 1.5 * current_atr,
take_profit=current_middle,
metadata={'rsi': current_rsi, 'bb_lower': current_lower, 'bb_middle': current_middle}
)
# Overbought at upper band
elif current_price >= current_upper and current_rsi > 70:
signal = Signal(
instrument=instrument,
direction='short',
strength=0.6 + (current_rsi - 70) / 100,
source='technical',
timestamp=df.index[-1],
entry_price=current_price,
stop_loss=current_price + 1.5 * current_atr,
take_profit=current_middle,
metadata={'rsi': current_rsi, 'bb_upper': current_upper, 'bb_middle': current_middle}
)
return signal
2.3 Economic Calendar Integration
@dataclass
class EconomicEvent:
"""Economic calendar event."""
event_id: str
name: str
currency: str
impact: str # 'low', 'medium', 'high'
datetime: datetime
actual: Optional[float] = None
forecast: Optional[float] = None
previous: Optional[float] = None
class EconomicCalendar:
"""Economic calendar for fundamental analysis."""
def __init__(self):
self.events: List[EconomicEvent] = []
def add_event(self, event: EconomicEvent):
"""Add an economic event."""
self.events.append(event)
self.events.sort(key=lambda x: x.datetime)
def get_upcoming_events(self, hours_ahead: int = 24) -> List[EconomicEvent]:
"""Get events in the next N hours."""
now = datetime.now()
cutoff = now + timedelta(hours=hours_ahead)
return [e for e in self.events if now <= e.datetime <= cutoff]
def get_high_impact_events(self, hours_ahead: int = 24) -> List[EconomicEvent]:
"""Get high impact events."""
upcoming = self.get_upcoming_events(hours_ahead)
return [e for e in upcoming if e.impact == 'high']
def should_avoid_trading(self, instrument: str, minutes_before: int = 30) -> Tuple[bool, Optional[EconomicEvent]]:
"""Check if trading should be avoided due to upcoming event."""
# Extract currencies from instrument
base = instrument[:3]
quote = instrument[3:6] if len(instrument) >= 6 else instrument[4:]
now = datetime.now()
event_window = now + timedelta(minutes=minutes_before)
for event in self.events:
if event.impact == 'high' and event.currency in [base, quote]:
if now <= event.datetime <= event_window:
return True, event
return False, None
# Create sample economic calendar
def create_sample_calendar() -> EconomicCalendar:
calendar = EconomicCalendar()
# Add sample events
events = [
EconomicEvent('EVT001', 'Non-Farm Payrolls', 'USD', 'high', datetime.now() + timedelta(hours=2)),
EconomicEvent('EVT002', 'ECB Rate Decision', 'EUR', 'high', datetime.now() + timedelta(hours=5)),
EconomicEvent('EVT003', 'UK CPI', 'GBP', 'medium', datetime.now() + timedelta(hours=12)),
EconomicEvent('EVT004', 'US Retail Sales', 'USD', 'medium', datetime.now() + timedelta(hours=26)),
]
for event in events:
calendar.add_event(event)
return calendar
Part 3: Risk Management
Implement comprehensive risk management for leveraged trading.
class RiskManager:
"""Comprehensive risk management for forex/futures."""
def __init__(self, max_risk_per_trade: float = 0.02,
max_daily_loss: float = 0.05,
max_positions: int = 5,
max_correlation_exposure: float = 0.5):
self.max_risk_per_trade = max_risk_per_trade
self.max_daily_loss = max_daily_loss
self.max_positions = max_positions
self.max_correlation_exposure = max_correlation_exposure
self.daily_pnl = 0.0
self.daily_start_balance = 0.0
self.last_reset = None
# Currency pair correlations (simplified)
self.correlations = {
('EURUSD', 'GBPUSD'): 0.85,
('EURUSD', 'USDCHF'): -0.90,
('AUDUSD', 'NZDUSD'): 0.90,
('USDJPY', 'EURJPY'): 0.75,
}
def reset_daily(self, current_balance: float):
"""Reset daily tracking."""
self.daily_pnl = 0.0
self.daily_start_balance = current_balance
self.last_reset = datetime.now()
def calculate_position_size(self, account_balance: float, entry_price: float,
stop_loss: float, pip_value: float = 10.0) -> float:
"""Calculate position size based on risk."""
risk_amount = account_balance * self.max_risk_per_trade
stop_distance_pips = abs(entry_price - stop_loss) * 10000
if stop_distance_pips == 0:
return 0
# Position size in lots (100,000 units)
position_lots = risk_amount / (stop_distance_pips * pip_value)
# Round to micro lots (0.01)
return round(position_lots, 2)
def can_open_trade(self, broker: SimulatedBroker, signal: Signal) -> Tuple[bool, str]:
"""Check if a new trade can be opened."""
account = broker.get_account_summary()
# Check daily loss limit
daily_loss_pct = abs(self.daily_pnl) / self.daily_start_balance if self.daily_start_balance > 0 else 0
if self.daily_pnl < 0 and daily_loss_pct >= self.max_daily_loss:
return False, f"Daily loss limit reached: {daily_loss_pct*100:.1f}%"
# Check max positions
if account['open_positions'] >= self.max_positions:
return False, f"Max positions reached: {account['open_positions']}"
# Check margin level
if account['margin_level'] < 200:
return False, f"Low margin level: {account['margin_level']:.0f}%"
# Check correlation exposure
if not self._check_correlation(broker.positions, signal.instrument):
return False, "Correlation exposure too high"
return True, "OK"
def _check_correlation(self, positions: Dict[str, Position], new_instrument: str) -> bool:
"""Check if new position would create too much correlation exposure."""
for inst in positions:
pair = tuple(sorted([inst.replace('_', ''), new_instrument.replace('_', '')]))
correlation = self.correlations.get(pair, 0)
if abs(correlation) > self.max_correlation_exposure:
# Check if same direction for positive correlation
return False
return True
def update_daily_pnl(self, pnl: float):
"""Update daily P&L tracking."""
self.daily_pnl += pnl
def get_risk_metrics(self, broker: SimulatedBroker) -> Dict:
"""Get current risk metrics."""
account = broker.get_account_summary()
return {
'daily_pnl': self.daily_pnl,
'daily_pnl_pct': (self.daily_pnl / self.daily_start_balance * 100) if self.daily_start_balance > 0 else 0,
'daily_loss_limit_remaining': max(0, self.max_daily_loss * self.daily_start_balance + self.daily_pnl),
'margin_level': account['margin_level'],
'open_positions': account['open_positions'],
'max_positions': self.max_positions,
'equity': account['equity']
}
Part 4: Trading Engine
Build the core trading engine that orchestrates all components.
class TradingEngine:
"""
Core trading engine orchestrating all components.
Components:
- Market data management
- Signal generation
- Risk management
- Order execution
- Performance tracking
"""
def __init__(self, config: Dict):
self.config = config
# Initialize components
self.market_data = MarketDataManager()
self.broker = SimulatedBroker(
initial_balance=config.get('initial_balance', 10000),
leverage=config.get('leverage', 50)
)
self.risk_manager = RiskManager(
max_risk_per_trade=config.get('risk_per_trade', 0.02),
max_daily_loss=config.get('max_daily_loss', 0.05),
max_positions=config.get('max_positions', 5)
)
self.calendar = create_sample_calendar()
# Signal generators
self.signal_generators: List[SignalGenerator] = [
TrendFollowingSignal(),
MeanReversionSignal()
]
# Trading parameters
self.instruments = config.get('instruments', ['EURUSD', 'GBPUSD', 'USDJPY'])
self.timeframe = config.get('timeframe', 'H1')
# State
self.running = False
self.paused = False
self.signals_generated = 0
self.trades_executed = 0
# Logging
logging.basicConfig(level=logging.INFO)
self.logger = logging.getLogger('TradingEngine')
def start(self):
"""Start the trading engine."""
self.running = True
self.risk_manager.reset_daily(self.broker.balance)
self.logger.info("Trading engine started")
def stop(self):
"""Stop the trading engine."""
self.running = False
self.broker.close_all_positions(self.market_data)
self.logger.info("Trading engine stopped")
def pause(self):
"""Pause trading."""
self.paused = True
self.logger.info("Trading paused")
def resume(self):
"""Resume trading."""
self.paused = False
self.logger.info("Trading resumed")
def on_tick(self, tick: Tick):
"""Process new tick data."""
if not self.running:
return
# Update market data
self.market_data.update_tick(tick)
# Update positions
self.broker.update_positions(self.market_data)
def on_candle_close(self, candle: OHLC):
"""Process candle close - main signal generation."""
if not self.running or self.paused:
return
# Add candle to market data
self.market_data.add_candle(candle)
# Skip if already in position for this instrument
if candle.instrument in self.broker.positions:
return
# Check economic calendar
should_avoid, event = self.calendar.should_avoid_trading(candle.instrument)
if should_avoid:
self.logger.info(f"Avoiding {candle.instrument} due to {event.name}")
return
# Get price data
df = self.market_data.get_ohlc_dataframe(candle.instrument, candle.timeframe)
if df.empty:
return
# Generate signals
best_signal = None
best_strength = 0
for generator in self.signal_generators:
signal = generator.generate(df, candle.instrument)
if signal and signal.strength > best_strength:
best_signal = signal
best_strength = signal.strength
if best_signal and best_strength >= self.config.get('min_signal_strength', 0.6):
self.signals_generated += 1
self._process_signal(best_signal)
def _process_signal(self, signal: Signal):
"""Process and potentially execute a signal."""
# Check if we can trade
can_trade, reason = self.risk_manager.can_open_trade(self.broker, signal)
if not can_trade:
self.logger.info(f"Signal rejected: {reason}")
return
# Calculate position size
account = self.broker.get_account_summary()
position_lots = self.risk_manager.calculate_position_size(
account['equity'],
signal.entry_price,
signal.stop_loss
)
if position_lots < 0.01:
self.logger.info("Position size too small")
return
# Convert lots to units
units = position_lots * 100000
# Get current tick
tick = self.market_data.get_price(signal.instrument)
if not tick:
return
# Submit order
side = OrderSide.BUY if signal.direction == 'long' else OrderSide.SELL
order = self.broker.submit_order(
instrument=signal.instrument,
side=side,
units=units,
order_type=OrderType.MARKET,
stop_loss=signal.stop_loss,
take_profit=signal.take_profit,
current_tick=tick
)
if order.status == 'filled':
self.trades_executed += 1
self.logger.info(f"Trade executed: {signal.instrument} {signal.direction} @ {order.fill_price}")
def get_status(self) -> Dict:
"""Get current engine status."""
account = self.broker.get_account_summary()
risk = self.risk_manager.get_risk_metrics(self.broker)
return {
'running': self.running,
'paused': self.paused,
'account': account,
'risk': risk,
'signals_generated': self.signals_generated,
'trades_executed': self.trades_executed,
'open_positions': len(self.broker.positions),
'total_trades': len(self.broker.trade_history)
}
Part 5: Performance Tracking & Journaling
Implement automated performance tracking and trade journaling.
class PerformanceTracker:
"""Track and analyze trading performance."""
def __init__(self, engine: TradingEngine):
self.engine = engine
self.daily_snapshots: List[Dict] = []
def calculate_statistics(self) -> Dict:
"""Calculate comprehensive statistics."""
trades = self.engine.broker.trade_history
if not trades:
return {'total_trades': 0}
winners = [t for t in trades if t.pnl > 0]
losers = [t for t in trades if t.pnl <= 0]
total_pnl = sum(t.pnl for t in trades)
# Calculate metrics
win_rate = len(winners) / len(trades) * 100
avg_win = np.mean([t.pnl for t in winners]) if winners else 0
avg_loss = np.mean([t.pnl for t in losers]) if losers else 0
gross_profit = sum(t.pnl for t in winners)
gross_loss = abs(sum(t.pnl for t in losers))
profit_factor = gross_profit / gross_loss if gross_loss > 0 else float('inf')
# Calculate max drawdown
equity_curve = [self.engine.broker.initial_balance]
for trade in trades:
equity_curve.append(equity_curve[-1] + trade.pnl)
peak = equity_curve[0]
max_dd = 0
for equity in equity_curve:
if equity > peak:
peak = equity
dd = (peak - equity) / peak * 100
max_dd = max(max_dd, dd)
# Calculate Sharpe ratio (simplified daily)
if len(trades) > 1:
returns = [t.pnl for t in trades]
sharpe = np.mean(returns) / np.std(returns) * np.sqrt(252) if np.std(returns) > 0 else 0
else:
sharpe = 0
return {
'total_trades': len(trades),
'winners': len(winners),
'losers': len(losers),
'win_rate': win_rate,
'total_pnl': total_pnl,
'avg_win': avg_win,
'avg_loss': avg_loss,
'profit_factor': profit_factor,
'max_drawdown': max_dd,
'sharpe_ratio': sharpe,
'return_pct': (self.engine.broker.balance / self.engine.broker.initial_balance - 1) * 100
}
def generate_report(self) -> str:
"""Generate performance report."""
stats = self.calculate_statistics()
account = self.engine.broker.get_account_summary()
lines = [
"\n" + "=" * 60,
"TRADING SYSTEM PERFORMANCE REPORT",
f"Generated: {datetime.now().strftime('%Y-%m-%d %H:%M')}",
"=" * 60,
"",
"ACCOUNT SUMMARY",
"-" * 40,
f" Initial Balance: ${self.engine.broker.initial_balance:,.2f}",
f" Current Balance: ${account['balance']:,.2f}",
f" Equity: ${account['equity']:,.2f}",
f" Return: {stats.get('return_pct', 0):.2f}%",
"",
"TRADING STATISTICS",
"-" * 40,
f" Total Trades: {stats.get('total_trades', 0)}",
f" Winners: {stats.get('winners', 0)}",
f" Losers: {stats.get('losers', 0)}",
f" Win Rate: {stats.get('win_rate', 0):.1f}%",
f" Average Win: ${stats.get('avg_win', 0):.2f}",
f" Average Loss: ${stats.get('avg_loss', 0):.2f}",
f" Profit Factor: {stats.get('profit_factor', 0):.2f}",
"",
"RISK METRICS",
"-" * 40,
f" Max Drawdown: {stats.get('max_drawdown', 0):.2f}%",
f" Sharpe Ratio: {stats.get('sharpe_ratio', 0):.2f}",
f" Open Positions: {account['open_positions']}",
f" Margin Level: {account['margin_level']:.0f}%",
"",
"=" * 60
]
return "\n".join(lines)
Part 6: Running the System
Demonstrate the complete trading system.
def generate_sample_data(instrument: str, days: int = 30) -> Tuple[List[OHLC], List[Tick]]:
"""Generate sample market data for testing."""
np.random.seed(42)
# Starting prices
start_prices = {
'EURUSD': 1.0850,
'GBPUSD': 1.2650,
'USDJPY': 149.50
}
price = start_prices.get(instrument, 1.0)
candles = []
ticks = []
start_date = datetime.now() - timedelta(days=days)
for day in range(days):
for hour in range(24):
# Generate hourly candle
timestamp = start_date + timedelta(days=day, hours=hour)
# Random walk with slight trend
returns = np.random.randn() * 0.001 + 0.00002 # Slight upward bias
open_price = price
close_price = price * (1 + returns)
high_price = max(open_price, close_price) * (1 + abs(np.random.randn() * 0.0005))
low_price = min(open_price, close_price) * (1 - abs(np.random.randn() * 0.0005))
candle = OHLC(
instrument=instrument,
timeframe='H1',
timestamp=timestamp,
open=open_price,
high=high_price,
low=low_price,
close=close_price,
volume=np.random.randint(1000, 5000)
)
candles.append(candle)
# Generate tick at candle close
spread = 0.0002
tick = Tick(
instrument=instrument,
bid=close_price - spread/2,
ask=close_price + spread/2,
timestamp=timestamp
)
ticks.append(tick)
price = close_price
return candles, ticks
# Configure and run the trading system
config = {
'initial_balance': 10000,
'leverage': 50,
'risk_per_trade': 0.02,
'max_daily_loss': 0.05,
'max_positions': 3,
'instruments': ['EURUSD', 'GBPUSD', 'USDJPY'],
'timeframe': 'H1',
'min_signal_strength': 0.6
}
# Create trading engine
engine = TradingEngine(config)
performance = PerformanceTracker(engine)
# Generate sample data
print("Generating sample data...")
all_candles = []
all_ticks = []
for instrument in config['instruments']:
candles, ticks = generate_sample_data(instrument, days=60)
all_candles.extend(candles)
all_ticks.extend(ticks)
# Sort by timestamp
all_candles.sort(key=lambda x: x.timestamp)
all_ticks.sort(key=lambda x: x.timestamp)
print(f"Generated {len(all_candles)} candles and {len(all_ticks)} ticks")
# Run backtest
print("\nRunning backtest...")
engine.start()
for i, (candle, tick) in enumerate(zip(all_candles, all_ticks)):
# Process tick
engine.on_tick(tick)
# Process candle close
engine.on_candle_close(candle)
# Progress update
if (i + 1) % 500 == 0:
status = engine.get_status()
print(f"Processed {i+1}/{len(all_candles)} candles, "
f"Signals: {status['signals_generated']}, "
f"Trades: {status['trades_executed']}, "
f"Balance: ${status['account']['balance']:,.2f}")
engine.stop()
print("\nBacktest complete!")
# Generate performance report
report = performance.generate_report()
print(report)
# Plot equity curve
if engine.broker.trade_history:
equity_curve = [engine.broker.initial_balance]
dates = [engine.broker.trade_history[0].entry_time]
for trade in engine.broker.trade_history:
equity_curve.append(equity_curve[-1] + trade.pnl)
dates.append(trade.exit_time)
plt.figure(figsize=(12, 6))
plt.plot(dates, equity_curve, 'b-', linewidth=1.5)
plt.axhline(y=engine.broker.initial_balance, color='gray', linestyle='--', alpha=0.5)
plt.fill_between(dates, equity_curve, engine.broker.initial_balance,
where=[e >= engine.broker.initial_balance for e in equity_curve],
alpha=0.3, color='green')
plt.fill_between(dates, equity_curve, engine.broker.initial_balance,
where=[e < engine.broker.initial_balance for e in equity_curve],
alpha=0.3, color='red')
plt.title('Trading System Equity Curve')
plt.xlabel('Date')
plt.ylabel('Equity ($)')
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
else:
print("No trades to plot")
Capstone Completion Checklist
Ensure your system includes:
Core Components
- [ ] Market data management (ticks and candles)
- [ ] Simulated broker with realistic execution
- [ ] Position and order management
- [ ] Multiple instrument support
Signal Generation
- [ ] At least 2 technical strategies
- [ ] Economic calendar integration
- [ ] Signal strength filtering
Risk Management
- [ ] Position sizing based on risk
- [ ] Daily loss limits
- [ ] Maximum position limits
- [ ] Correlation monitoring
Performance Tracking
- [ ] Trade history logging
- [ ] Performance statistics
- [ ] Equity curve tracking
- [ ] Automated reporting
Documentation
- [ ] Code comments
- [ ] Configuration documentation
- [ ] Usage examples
Congratulations!
You have completed the Forex & Futures Trading course capstone project. You now have a complete, production-ready trading system framework that demonstrates:
- Understanding of forex and futures markets
- Technical and fundamental analysis integration
- Professional risk management practices
- Software engineering skills for trading systems
Next Steps: 1. Extend with additional strategies 2. Connect to live broker APIs (OANDA, MT5) 3. Add machine learning signal filters 4. Implement real-time alerting 5. Deploy on cloud infrastructure